spring security 认证流程理解
最近正好做项目的认证授权模块,用到了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");
}
}
上一篇: Kubernetes API 的访问控制
下一篇: 紧固件CE认证
推荐阅读
-
spring security 认证流程理解
-
spring-boot整合spring-security,Swagger使用spring-security认证
-
spring-security(二十三)Remember-Me认证 博客分类: spring security springsecurity
-
spring-security(二十二)基本认证和摘要认证 博客分类: spring security springsecurity
-
spring-security(十)基本认证过程 博客分类: spring security springsecurity
-
Spring Security认证提供程序示例详解
-
详解最简单易懂的Spring Security 身份认证流程讲解
-
2 Spring Security详解(认证用户)
-
spring security自定义认证登录的全过程记录
-
Spring Security OAuth2认证授权示例详解