一、什么是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;
    }
}