欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring security 认证流程理解

程序员文章站 2024-03-19 13:27:34
...

最近正好做项目的认证授权模块,用到了spring security 。就简单的认证授权方式,个人还是感觉手动实现一套认证授权逻辑比较方便。但可以借鉴spring security 认证的思想。

spring security 的认证授权依靠一系列过滤器,比较重要的几个过滤器依次为SecurityContextPersistenceFilter,LogoutFilter,UsernamePasswordAuthenticationFilter ,AnonymousAuthenticationFilter。

SecurityContextPersistenceFilter:请求到来的时候,会检查session中是否存在认证信息,security中认证信息被封装在一个Authentication 里,Authentication 继承AbstractAuthenticationToken 。默认表单登陆的Authentication是UsernamePasswordAuthenticationToken,session中存在认证信息,则把认证信息放在SecurityContextHolder里,这是类既对ThreadLocal进行了封装。后续需要用到authentication的时候就从SecurityContextHolder里取。当请求完毕再次经过这个过滤器的时候,会检查SecurityContextHolder里是否存在认证信息,若存在,则把它放到session里。这里会产生一个问题,如果要求认证兼容app,session中存放authentication对于app开发过于繁琐,改用jwt的方式验证,这里后面会讲到。

LogoutFilter : 退出操作的过滤器,所做操作就是将SecurityContextHolder中authentication清理掉。

UsernamePasswordAuthenticationFilter:拦截表单登陆,会将前端传来的用户名密码信息封装成一个未认证的authentication ,然后交由AuthenticationManager 去认证这个信息。AuthenticationManager 会调用authentication对应的provider去处理认证逻辑。UsernamePasswordAuthenticationToken类型的默认交由DaoAuthenticationProvider处理,DaoAuthenticationProvider 是调用一个UserService(需要自定义一个类继承该接口)的loadByName返回原始用户信息,然后与
SecurityContextHolder中的authentication作比较,比较通过则把authentication的状态设为已认证状态,并赋予权限值。

AnonymousAuthenticationFilter:如果上一步中authentication认证不通过,那么这个过滤器就会将authention 的pricipal
赋予一个匿名用户类型,authentication的pricipal前端传过来的时候被UsernamePasswordAuthenticationFilter赋予用户名,经过认证后是一个UserDetail的对象(需要自己定义,继承接口)。

下面说下关于用jwt替换掉session持久认证我的思路。

1,关闭session 。直接在security的配置类中设置session的管理策略为SessionCreationPolicy.STATELESS即可。
2,增加一个过滤器,用于验证前端请求头部中是否含有token,如果有token,并且token与服务器端校验通过。则视为认证成功,模仿着security的做法,创建UsernamePasswordAuthenticationToken并将其状态变为以认证状态。放入SecurityContextHolder中。
3,具体做法是,后端第一次认证通过后,将生成token,放入响应头部中,并放入缓存中,以username作为key值,token做value值,并设置过期时间,下次请求过来,如果请求头部中含有token,则解析出其中username,和服务器端token做比较。
4,这里会产生一个问题,token值的过期时间应比较短,达到安全的目的。如果我按上述做法,token频繁失效,用户将频繁登陆,极度影响体验。解决思路,redis缓存中过期时间设置比较长,token字符串有个过期属性,这个比较短,当解析出token字符串时比较这个短期过期时间和缓存中的过期时间,如果短期时间过期,但缓存中没过期,则更新token,返回前端,让前端在发送一次请求,达到悄悄的登陆的目的。
下面是我过滤器的代码。过滤器需在security配置类中配置。操作jwt用的jjwt工具包。

@Component
@Slf4j
public class SecurityContextFilter extends OncePerRequestFilter {
    @Autowired
    private WebUserDetailService userDetailService;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain filterChain) throws ServletException, IOException {
        if (StringUtils.isBlank(req.getHeader("authentication"))) {
            doFilter(req,resp,filterChain);
            return;
        }
        Claims claims = null;
        String token = null;
        try {
            claims = jwtUtil.parseJwt(req.getHeader("authentication"));
        } catch (ExpiredJwtException e) {
            //token 过期
            claims = e.getClaims();
            //请求中的token
            token = req.getHeader("authentication");
            //1,判断refresh token  ttl 的过期时间 ,若超过 则 执行过滤链,
            long leftDays = stringRedisTemplate.getExpire(RedisConstants.getRedisKey(RedisConstants.JWT_TOKEN,
                    claims.get("username", String.class)));
            //2,超过refresh token  ttl 的过期时间
            if (leftDays < JwtConstants.REFRESH_TOKEN_TTL) {
                doFilter(req,resp,filterChain);
                return;
            }
            //3,没超过refresh token  ttl 的过期时间,返回新token 给 前端
            String username = claims.get("username", String.class);
            String password = claims.get("password", String.class);
            //判断 是否已经替换了refresh token
            if (stringRedisTemplate.opsForValue().get(RedisConstants.getRedisKey(RedisConstants.JWT_OLD_NEW_MAP, token)) != null) {
                //已经替换了refreshToken ,通过认证
                log.info("已经替换了refreshToken ,通过认证");
                persitenceAuthentication(claims, req);
                doFilter(req,resp,filterChain);
                return;
            }
            log.info("替换新的token");
            String refreshToken = jwtUtil.createToken(username, password);
            resp.setHeader("authentication",refreshToken);
            stringRedisTemplate.opsForValue().set(RedisConstants.getRedisKey(RedisConstants.JWT_TOKEN, username), refreshToken
                    , JwtConstants.REFRESH_TOKEN_TTL * 2, TimeUnit.DAYS);
            Map<String, String> resMap = new HashMap<>();
            stringRedisTemplate.opsForValue().set(RedisConstants.getRedisKey(RedisConstants.JWT_OLD_NEW_MAP, token), refreshToken,
                    30, TimeUnit.SECONDS);

            ResponseUtil.response(resp,1,refreshToken,1009);
            return;
        }
        //1,解析token 的username 判断redis中是否存在 ,不存在,执行过滤链
        //内存中的token
        token = stringRedisTemplate.opsForValue().get(RedisConstants.getRedisKey(RedisConstants.JWT_TOKEN,
                claims.get("username", String.class)));
        //token 被篡改
        if (token == null) {
            doFilter(req,resp,filterChain);
            return;
        }else {
            persitenceAuthentication(claims,req);
            doFilter(req,resp,filterChain);
            return;
        }
    }

    private void persitenceAuthentication(Claims claims, HttpServletRequest req) {
        UserDetails userDetails = userDetailService.loadUserByUsername(claims.get("username", String.class));
        if (userDetails != null) {
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                    = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        }
    }
}

jwtUtil

@Component
@Slf4j
public class JwtUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 生成token 并放到redis中
     * @param username
     * @param password
     * @return
     */
    public String createToken(String username,String password){
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillions = System.currentTimeMillis();
        Date date = new Date(nowMillions);
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username);
        claims.put("password",password);
        SecretKey key = getSecretKey();
        JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
                .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(UUID.randomUUID().toString())                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(date)           // iat: jwt的签发时间
                .setIssuer("heyoufu")          // issuer:jwt签发人
                .setSubject(username)//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
        builder.setExpiration(new Date(nowMillions+JwtConstants.JWT_TTL));
        return builder.compact();
    }




    public Claims parseJwt(String jwtToken){
        SecretKey key = getSecretKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //设置签名的秘钥
                .parseClaimsJws(jwtToken).getBody();     //设置需要解析的jwt
        return claims;
    }

    /**
     * @return
     */
    private SecretKey getSecretKey(){
        //按base64 解码
        byte[] decodeBytes =Base64.decodeBase64(JwtConstants.JWT_SECRETE_KEY);

        return new SecretKeySpec(decodeBytes,"AES");
    }
}

相关标签: 认证