springSecurity自定义登陆接口和JWT认证过滤器

下面我会根据该流程图去自定义接口:

我们需要做的任务有:

登陆:1、通过ProviderManager的方法进行认证,生成jwt;2、把用户信息存入redis;3、自定义UserDetailsService实现到数据库查询数据的方法。

校验:自定义一个jwt认证过滤器,其实现功能:获取token;解析token;从redis获取信息;存入SecurityContextHolder。

登陆:

图中的 5.1步骤是到内存中查询用户信息,而我们需要的是到数据库中查询。而图中查询用户信息是调用loadUserbyUsername方法实现的。

所以我们需要实现UserDetailsService接口并重写该方法:(下面案例中我用的mybatis plus实现的查询数据库)

java 复制代码
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    //loadUserByUsername方法即为流程图中查询用户信息的方法。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //查询用户信息
        LambdaQueryWrapper<User> queryWrapper= new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //如果没有查询到用户
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //封装为UserDetails类型返回
        return new LoginUser(user);
    }
}

我们先写好登陆功能的controller层代码:

java 复制代码
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        //登陆
        return loginService.login(user);
    }

我们需要让springSecurity对该登陆接口放行,不需要登陆就能访问。在登陆service层接口中需要通过AuthenticationManager的authenticate方法进行用户认证,我们先在SecurityConfig中把AuthenticationManager注入容器。

java 复制代码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean //(name = "") //获取AuthenticationManager的bean,因为现在只有这一个AuthenticationManager,所以不写也没事。
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    //放开接口
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                //关闭csrf,csrf为跨域策略,不支持post
                .csrf().disable()
                //不通过session获取SecurityContext 前后端分离时session不可用
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //对于登陆接口,允许匿名访问,登陆之后不允许访问,只允许匿名的用户,可以防止重复登陆。
                .antMatchers("/user/login").anonymous() //permitAll() 登录能访问,不登录也能访问,一般用于静态资源js等
                //除了上面外,所有请求需要鉴权认证
                .anyRequest().authenticated();//authenticated():任意用户,认证后都可访问。
    }

}

然后我们去修改登陆接口的service层实现类代理:

java 复制代码
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    //登陆
    @Override
    public ResponseResult login(User user) {
        //获取AuthenticationManager的authenticate方法进行认证。
        //通过SecurityConfig获取AuthenticationManager

        //创建Authentication,第一个参数为认证主体,没有的话传用户名,第二个参数传密码
        UsernamePasswordAuthenticationToken authenticationToken
                =new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());

        //需要Authentication参数(上面)
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //这样让ProviderManager调用UserDetailsService类中的loadUserByUsername方法完成认证
        //如果认证不通过,authenticate为null


        //认证没通过,给出提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登陆失败");
        }
        //认证通过,使用userid生成jwt,jwt存入ResponseResult返回
          //获取userId
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);

        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);

        //完整信息存入redis,userid作key
        redisCache.setCacheObject("login:"+userId,loginUser);

        return new ResponseResult(200,"登陆成功",map);
    }
}

springSecurity流程图中是通过获取AuthenticationManager的authenticate方法进行认证。通过SecurityConfig中注入的bean获取AuthenticationManager。

authenticationManager的authenticate方法需要一个Authentication实现类参数,所以我们创建一个UsernamePasswordAuthenticationToken实现类

其中的JwtUtil.createJWT(userId);方法,是我自定义的根据userId生成JWT的工具类方法:

java 复制代码
public class JwtUtil {
      
    //有效期为
    public static final Long JWT_TTL = 60*60*1000L;//一个小时
    //设置密钥明文 。随便定义,方便记忆和使用即可,但需要长度要为4的倍数。
    public static final String JWT_KEY = "jyue";
        

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    //生成JWT
    //subject为token中存放的数据(json格式)
    public static String createJWT(String subject){
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());//设置过期时间
        return builder.compact();
    }

    public 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 expMills=nowMillis+ttlMillis;
        Date expDate = new Date(expMills);
        return Jwts.builder()
                .setId(uuid) //唯一id
                .setSubject(subject) //主题 可以是JSON数据
                .setIssuer("jy") //签发者,随便写
                .setIssuedAt(now) //签发时间
                .signWith(signatureAlgorithm,secretKey) //使用HS256对称加密算法签名,第二个参数为密钥。
                .setExpiration(expDate);
    }
    public static SecretKey generalKey(){
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
        return key;
    }
    public static Claims parseJWT(String jwt)throws Exception{
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

redisCache也为自定义的redis工具类:

java 复制代码
@SuppressWarnings(value = {"unchecked","rawtypes"})
@Component
public class RedisCache {

    @Autowired
    public RedisTemplate redisTemplate;
 
    //缓存对象
    //key 缓存键值
    //value 缓存值
    public <T> void setCacheObject(final String key,final T value){
        redisTemplate.opsForValue().set(key,value);
    }

    //获取缓存的基本对象
    // key 键值
    // return 缓存键对应的数据
    public <T>T getCacheObject(final String key){
        ValueOperations<String,T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
}

JWT认证:

java 复制代码
@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");
        //判断token是否为Null
        if(!StringUtils.hasText(token)) {
            //token没有的话,直接放行,抛异常的活交给后续专门的过滤器。
            filterChain.doFilter(request, response);
            //响应时还会经过该过滤器一次,直接return,不能执行下面的解析token的代码。
            return;
        }
        //如何不为空,解析token,获得了UserId
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            //token格式异常,不是正经token
            throw new RuntimeException("token非法");
        }
        //根据UserId查redis获取用户数据
        String key = "login:"+userId;
        LoginUser loginUser = redisCache.getCacheObject(key);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //然后封装Authentication对象存入SecurityContextHolder
        // 因为后续的过滤器会从SecurityContextHolder中获取信息判断认证情况,而决定是否放行。
        // 这里用UsernamePasswordAuthenticationToken三个参数的构造函数,是因为其能设置已认证的状态(因为已经从redis中获取了信息,确认是认证的了)
        //第一个参数为用户信息,第三个参数为权限信息,目前还没获取,先填null
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        //默认SecurityContextHolder是ThreadLocal线程私有的,这也是为什么上面要用UsernamePasswordAuthenticationToken三个参数的构造方法
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request,response);
    }
}

这样登陆后用户发送请求,后端会先从请求头中获取token,然后解析出userId,然后从redis中查询该用户详细信息。然后把用户的详细信息存入UsernamePasswordAuthenticationToken三个参数的构造函数,是因为其能设置已认证的状态(因为已经从redis中获取了信息,确认是认证的了),然后把UsernamePasswordAuthenticationToken存入SecurityContextHolder。

因为后续的过滤器会从SecurityContextHolder中获取信息判断认证情况,而决定是否放行。

相关推荐
潜洋1 分钟前
Spring Boot 教程之三十六:实现身份验证
java·数据库·spring boot
科马12 分钟前
【Redis】缓存
数据库·redis·spring·缓存
LuiChun26 分钟前
Django 模板分割及多语言支持案例【需求文档】-->【实现方案】
数据库·django·sqlite
凡人的AI工具箱27 分钟前
每天40分玩转Django:Django管理界面
开发语言·数据库·后端·python·django
中科院提名者31 分钟前
Django连接mysql数据库报错ModuleNotFoundError: No module named ‘MySQLdb‘
数据库·mysql·django
Gauss松鼠会1 小时前
GaussDB数据库中SQL诊断解析之配置SQL限流
数据库·人工智能·sql·mysql·gaussdb
猿经验1 小时前
如何使用PSQL Tool还原pg数据库(sql格式)
数据库·sql
编程修仙1 小时前
MySQL外连接
数据库·mysql
Edward-tan2 小时前
【全栈开发】----用pymysql库连接MySQL,批量存入
数据库·mysql·pymysql
mxbb.2 小时前
单点Redis所面临的问题及解决方法
java·数据库·redis·缓存