一、什么是JWT
说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。
(1)、session所存在的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
(2)、Token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,也就是说token认证机制的应用不需要去考虑用户在哪一台服务器登录了。
(3)、认识Token
JWT是由三段信息构成的,以 点(.) 分割,每部分都有不同的含义(每段都是用 Base64 编码的)
第一部分为 头部(Header)
第二部分为 载荷(Payload)
第三部分为 签证(Signature)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNjI1NDY3MDY5LCJ1c2VyTmFtZSI6IumBlW_mCIsImlhdCI6MTYyNTQ2NTI2OX0.e_uuksv0b8gqX9HUVEiieLQlKFKcLdxCxovJ3xA3wB8
第一部分通过Base64解码出的结果是
{
"typ":"JWT",
"alg":"HS256"
}
由此可以得知jwt的头部承载两部分信息 类型和加密算法
第二部分是用来放主要的存储信息的(主要信息中除了自定义信息还有标准中注册的声明)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
当然以上是统一标准而已,并非必须用,建议不强制。
第三部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
二、使用JWT
(1)、导入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
(2)、创建JwtUtils工具类
@Value("{Jwt.secret}")
private static String secret;
/**
签发对象:随意
签发时间:现在
有效时间:30分钟
载荷内容:自定义内容
加密密钥:盐 + 密钥
*/
public static String createToken(String userId,String userName) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,30);
Date expiresDate = nowTime.getTime();
//签发对象
return JWT.create().withAudience(userId)
//发行时间
.withIssuedAt(new Date())
//有效时间
.withExpiresAt(expiresDate)
//载荷,随便写几个都可以,也可以理解为自定义参数
.withClaim("userName", userName)
//加密
.sign(Algorithm.HMAC256(secret+"你随意写"));
}
/**
* 检验合法性,其中secret参数就应该传入的是用户的id
* @param token
*/
public static void verifyToken(String token){
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"WDNMD")).build();
jwt = verifier.verify(token);
} catch (Exception e) {
//效验失败
//这里抛出的异常是我自定义的一个异常,你也可以写成别的
}
}
/**
* 获取签发对象
*/
public static String getAudience(String token) {
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//这里是token解析失败
}
return audience;
}
/**
* 通过载荷名字获取载荷的值
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}
三、JWT结合SpringSecurity实现登录鉴权以及权限管理
(1)、思路
登陆成功返回Token,并把Token存储到Redis中确保单点登录。使用过滤器校验Token和权限
(2)、SpringSecurity配置
由于使用Token进行登录鉴权,就不需要Session了,因此需禁用Session
@Component
@EnableWebSecurity
/**
* 开启@EnableGlobalMethodSecurity(prePostEnabled = true)注解
* 在继承 WebSecurityConfigurerAdapter 这个类的类上面贴上这个注解
* 并且prePostEnabled设置为true,@PreAuthorize这个注解才能生效
* SpringSecurity默认是关闭注解功能的.
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入过滤器
@Resource
private JwtVerificationFilter jwtVerificationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf防护 >只有关闭了,才能接受来自表单的请求
http.csrf().disable()
.cors()//开启跨域
.and()
//开启授权请求
.authorizeRequests()
//放行接口,因为使用自定义登录页面所以需要放行
.antMatchers("/login/**").permitAll()
//拦截所有请求,所有请求都需要登录认证
.anyRequest().authenticated()
.and()
.addFilterAfter(jwtVerificationFilter, UsernamePasswordAuthenticationFilter.class)
//前后端分离采用JWT 不需要session(添加后Spring永远不会创建session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
(3)、编写过滤器
/**
* @author admin
* 过滤器 发起请求前检验Token 实现并在每次请求时只执行一次过滤
* 在spring中,filter都默认继承OncePerRequestFilter
* OncePerRequestFilter顾名思义,他能够确保在一次请求只通过一次filter,而不需要重复执行
* 为了兼容不同的web container,特意而为之
*
* 在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况
*
* servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤,
*/
@Component
@Slf4j
public class JwtVerificationFilter extends OncePerRequestFilter {
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
@Resource
private RolePermissionService rolePermissionService;
/**
* 过滤器,检验Token
* 发起请求时会调用两次,第二次是展示/favicon.ico
*
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain) throws ServletException, IOException {
//获取Token
String token = httpServletRequest.getHeader("token");
//非空校验
if (token == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//检验Token合法性
JwtUtils.getAudience(token);
//比对Redis中存储的Token
String redisToken = RedisUtils.get(RedisPrefixKey.LOGIN_TOKEN.keyAppend(JwtUtils.getAudience(token)).getKey())
.toString();
if (!redisToken.equals(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//获取权限 根据Token获取载荷的值
List<GrantedAuthority> authorityList = this.findAllAuthority(Long.valueOf(JwtUtils.getAudience(token)));
//安全上下文,存储认证授权的相关信息,实际上就是存储"当前用户"账号信息和相关权限
SecurityContextHolder
.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(null,null,authorityList));
//将请求转发给过滤器链下一个filter
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 查找权限
*/
private List<GrantedAuthority> findAllAuthority(Long userId){
//1、拿到用户的角色和权限
//2、返回的权限
List<GrantedAuthority> authorityList = new ArrayList<>();
//3、查出权限列表循环放入 authorityList 中
for (权限实体类 url : 权限集合) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(权限的url);
authorityList.add(simpleGrantedAuthority);
}
return authorityList;
}
}