目录
一. 快速入门
二. 认证
2.1 登陆校验流程
2.2 原理初探
2.3 解决问题
2.3.1 思路分析
2.3.2 准备工作
2.3.3 实现
2.3.3.1 数据库校验用户
2.3.3.2 密码加密存储
2.3.3.3 登录接口
2.3.3.4 认证过滤器
2.3.3.5 退出登录
Spring Security是Spring家族中的一个安全管理框架,相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说大型项目用Spring Security比较多,小项目用Shiro比较多,因为相比于Spring Security,Shiro上手比较简单。
一般Web应用需要进行认证和授权。
- 认证:验证当前访问系统的是不是本系统用户,并且要确认具体是哪个用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权正是Spring Security作为安全框架的核心功能!
一. 快速入门
我们先简单构建出一个SpringBoot项目。
这个时候我们访问我们写的一个简单的hello接口,验证是否构建成功。
接着引入SpringSecurity。
这个时候我们再看看访问接口的效果。
引入了SpringSecurity之后,访问接口会自动跳转到一个登录页面,默认的用户名是user,密码会输出到控制台,必须登录后才能对接口进行访问。
二. 认证
2.1 登陆校验流程
首先我们要先了解登录校验流程,首先前端携带用户名和密码访问登录接口,服务器拿到这个用户名和密码之后去和数据库中的进行比较,如果正确使用用户名/用户ID,生成一个jwt,接着把jwt响应给前端,之后登录后访问其他的请求都会在请求头中携带token,服务器每次获取请求头中的token进行解析、获取UserID,根据用户名id获取用户相关信息,查看器权限,如果有权限则响应给前端。
2.2 原理初探
SpringSecurity的原理其实就是一个过滤器链,内部提供了各种功能的过滤器,这里我们先看看上方快速入门中涉及的过滤器。
- UsernamePasswordAuthenticationFilter负责处理在登录页面填写的用户名密码后的登录请求
- ExceptionTranslationFilter处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
- FilterSecurityInterceptor负责权限校验的过滤器
我们也可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器以及顺序。
接下来我们来看看认证流程图的解析。
这里我们只需要能看懂其过程即可,简单来说就是:
用户提交了用户名和密码,UsernamePasswordAuthenticationFilter将其封装未Authentication对象,并且调用authenticate方法进行认证,接着在调用DaoAuthenticationProvider的authenticate方法进行认证,再调用loadUserByUserName方法查询用户,这里的查询是在内存中进行查找,然后将对应的用户信息封装未UserDetails对象,通过PasswordEncoder对比UserDetails中的密码和Authentication的密码是否正确,如果正确就把UserDetails中的权限信息设置到Authentication对象中,接着返回Authentication对象,最后使用SecurityContextHolder.getContext().setAuthentication方法存储该对象,其他过滤器会通过SecurityContextHoder来获取当前用户信息。(这一段不用记忆能听懂即可)
那么我们知道了其过程,才能对其进行修改,首先这里的从内存中查找,我们肯定是要该为从数据库中查找(这里需要我们自定义一个UserDetailsService的实现类),并且也不会使用默认的用户名密码,登录界面也一定是自己编写的,不需要用他提供的默认登录页面。
基于我们分析的情况,可以得到这样的一张图。
这个时候就返回了一个jwt给前端,而这时前端进行的其他请求都会携带token,那么我们第一步就需要先校验是否携带token,并且解析token,获取对应的userid,并且将其封装为Anthentication对象存入SecurityContextHolder(为了其他过滤器可以拿到)。
那么这里还有一个问题,从jwt认证过滤器中取到了userid后如何获取完整的用户信息?
这里我们使用redis,当服务器认证通过使用用户id生成jwt给前端的时候,以用户id作为key,用户的信息作为value存入redis,之后就可以通过userid从redis中获取到完整的用户信息了。
2.3 解决问题
2.3.1 思路分析
从上述的原理初探中,我们也大概分析出了我们要是自己实现前后端分离的认证流程,需要做的事情。
登录:
a.自定义登录接口
调用ProviderManager的方法进行认证,如果认证通过生成jwt
把用户信息存入redis中
b.自定义UserDetailsService
在这个实现类中去查询数据库
校验:
a.自定义jwt认证过滤器
获取token
解析token获取其中userid
从redis中获取完整用户信息
存入SecurityContextHolder
2.3.2 准备工作
首先需要添加对应的依赖
<!-- SpringSecurity启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- fastjson依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
接着我们需要用到Redis需要加入Redis相关的配置
首先是FastJson的序列化器
package org.example.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* Redis使用fastjson序列化
* @param <T>
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class<T> clazz){
super();
this.clazz=clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null){
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes==null || bytes.length<=0){
return null;
}
String str = new String(bytes,DEFAULT_CHARSET);
return JSON.parseObject(str,clazz);
}
protected JavaType getJavaType(Class<?> clazz){
return TypeFactory.defaultInstance().constructType(clazz);
}
}
创建RedisConfig在其中创建序列化器,解决乱码等问题
package org.example.config;
import org.example.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = {"unchecked","rawtypes"})
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
//使用StringRedisSerializer来序列化和反序列化redus的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
还需要统一响应类
package org.example.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T>{
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private String msg;
/**
* 查询到的结果数据
*/
private T data;
public ResponseResult(Integer code,String msg){
this.code = code;
this.msg = msg;
}
public ResponseResult(Integer code,T data){
this.code = code;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public ResponseResult(Integer code,String msg,T data){
this.code = code;
this.msg = msg;
this.data = data;
}
}
再需要jwt的工具类用于生成jwt,以及对jwt进行解析
package org.example.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60*60*1000L; //一个小时
//设置密钥明文
public static final String JWT_KEY = "hzj";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-","");
return token;
}
/**
* 生成jwt
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject){
JwtBuilder builder = getJwtBuilder(subject,null,getUUID()); //设置过期时间
return builder.compact();
}
/**
* 生成jwt
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject,Long ttlMillis){
JwtBuilder builder = getJwtBuilder(subject,ttlMillis,getUUID()); //设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject,Long ttlMillis,String uuid){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalkey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的Id
.setSubject(subject) //主题 可以是Json数据
.setIssuer("hzj") //签发者
.setIssuedAt(now) //签发时间
.signWith(signatureAlgorithm,secretKey) //使用HS256对称加密算法签名,第二个参数为密钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id,String subject,Long ttlMillis){
JwtBuilder builder = getJwtBuilder(subject,ttlMillis,id);//设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception{
String token =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTg0MjU5MzIsInVzZX" +
"JJZCI6MTExLCJ1c2VybmFtZSI6Ik1hcmtaUVAifQ.PTlOdRG7ROVJqPrA0q2ac7rKFzNNFR3lTMyP_8fIw9Q";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的密钥secretkey
* @return
*/
public static SecretKey generalkey(){
byte[] encodeedkey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodeedkey,0,encodeedkey.length,"AES");
return key;
}
/**
* 解析
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception{
SecretKey secretKey = generalkey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
再定义一个Redis的工具类RedisCache,这样可以使我们调用redistemplate更加简单
package org.example.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
我们还有可能往响应中写入数据,那么就还需要一个工具类WebUtils
package org.example.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class WebUtils {
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
最后写对应的用户实体类
package org.example.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 用户表(User)实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
根据我们上方的分析我们是需要自定义一个UserDetailsService,让SpringSecuriry使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
我们先建立一个数据库表sys_user。
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '呢称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(O管理员,1普通用户)',
`create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标志(O代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
接着引入myBatisPlus和mysql驱动。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
接着配置数据库的相关信息。
接着定义mapper接口UserMapper,使用mybatisplus加入对应的注解。
package org.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.domain.User;
public interface UserMapper extends BaseMapper<User> {
}
接着配置组件扫描
最后测试一下mp能否正常使用。
引入junit
这样就是可以正常使用了。
2.3.3 实现
2.3.3.1 数据库校验用户
接下来我们需要进行核心代码的实现。
首先我们先进行自定义UserDetailsService。
package org.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.example.domain.LoginUser;
import org.example.domain.User;
import org.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息 [InMemoryUserDetailsManager是在内存中查找]
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就抛出异常,给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误!");
}
//TODO 查询权限信息
//封装为UserDetails对象返回
return new LoginUser(user);
}
}
这里要将user封装为UserDetails进行返回。
package org.example.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
最后这里有一个点,就是我们需要进行登录从数据库拿数据的测试,需要往表中写入用户数据,并且如果你想让用户的密码是明文传输,需要在密码前加上{noop}。
这里就实现了输入数据库中的用户名密码进行登录了。
2.3.3.2 密码加密存储
这里说一下为什么要在密码前面加上{noop},因为默认使用的PasswordEncoder要求数据库中的密码格式为{id}password,它会根据id去判断密码的加密方式,但是我们一般不会采取这种方式,所以就需要替换掉PasswordEncoder。
接下来我们进行测试看看。
可以看到我们这里传入的两次密码原文是一样的,但是却得到了不同的结果,这里其实和加盐算法有关,之后我还会写一个自定义加密的文章。
得到加密之后的密码之后就可以将加密后的密码存入数据库,之后可以由前端传过来的明文密码与数据库中的加密后的密码进行验证进行登录。
这个时候我们启动项目去登录,发现之前的密码已经登不上了,因为数据库此时存放的应该是注册阶段存入数据库的加密后的密码,而不是原文密码了(因为没注册我将加密后的密码自行写入数据库中)。
2.3.3.3 登录接口
我们需要实现一个登录接口,然后让SpringSecuruty对其进行放行,如果不放行就自相矛盾了,在接口中通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话需要生成一个jwt,放入响应中,并且为了让用户下次请求时能通过jwt识别出具体是哪个用户,需要把用户信息存入redis,可以把用户id作为key。
先写LoginController
接着写对应的Service。
在SecurityConfig中进行AuthenticationManager的注入,和登录接口的放行。
在service中的业务逻辑中,如果认证失败则返回一个自定义异常,但是如果认证成功我们需要如何获取到对应的信息呢。
这里我们可以debug看看得到的对象。
这里发现是Principal中可以得到对应需要的信息。
接着补全代码。
最后进行测试看看。
2.3.3.4 认证过滤器
我先将代码贴上。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response); //这里放行是因为还有后续的过滤器会给出对应的异常
return; //token为空 不执行后续流程
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法!");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录!");
}
//将信息存入SecurityContextHolder(因为过滤器链后面的filter都是从中获取认证信息进行对应放行)
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response); //此时的放行是携带认证的,不同于上方token为空的放行
}
}
首先这里获取token我们是从请求头中获取对应的token,然后对其进行判空,如果为空我们直接进行放行,且不走后续流程,接下来进行解析token,得到里面的userid,再根据userid从redis中获取对应的用户信息,最后将其存储到SecurityContextHolder中,因为后续的过滤器都需要从中获取日认证信息,最后进行分析操作。
还有一个需要注意的点就是,SecurityContextHolder.getContext().setAuthentication()需要传入authentication对象,我们构建对象的时候采用的是三个参数的,因为第三个参数是判断是否认证的关键。
接下来我们需要将这个过滤器进行配置。
接着我们进行访问user/login接口会返回给我们一个带token的响应体,再访问hello接口此时是403的,因为没有携带token,所以就对应上方的代码,没有token放行并且return不执行后续流程(这里的放行是因为后续有其他专门抛异常的过滤器进行处理,而return是为了不让其走响应的流程)
此时若我们将user/login生成的token放入hello接口的请求头那么就可以正常访问到了。
那么我们这套过滤器的目的也就达到了(获取token、解析token、存入SecurityContextHolder)
2.3.3.5 退出登录
到这里我们也就比较容易的实现退出登录了,我们只需要删除redis中对应的数据,之后携带token进行访问的时候,在我们自定义的过滤器中会获取redis中对应的用户信息,此时获取不到就意味着未登录。
我们携带这个token去访/user/logout接口。
那么退出登录功能就实现了。
本文学习于b站up主三更!!!