JWT-shiro-redis分布式会话共享

344 阅读6分钟

JWT:

生成JWT(加密)

可以看到登录成功之后,调用了jwtUtils.generateToken(User.getId())根据userId生成一个完整的jwt字符串,并把该jwt字符串放在了响应头中 有个疑问,客户端收到响应头以后会把这个jwt放在cookie中吗?-->会,但是并不是以后每次发起请求都会携带jwt发出请求,携带与否是前端决定。

image.png

image.png

image.png

解析JWT(解密)

从请求头获取到jwt以后,调用jwtUtils.getClaimByToken(jwt)对加密过的jwt进行解密,解密得到Claims对象, image.png

image.png

在加密过程中,我们把userId放在了Claims对象的SUBJECT属性中 image.png

Shiro逻辑分析:

对每一次请求都进行过滤,分为携带jwt与不携带jwt的情况

image.png

如果不携带jwt的话过滤器直接放行,如果携带jwt的话进入Shiro的登录认证 image.png

重写createToken(request,response)方法 image.png

自定义Realm,实现doGetAuthenticationInfo()与doGetAuthorizationInfo() image.png

image.png

image.png

image.png

由于响应要封装为统一结果类Result,所以重写onLoginFailure()

image.png

image.png

关于shiro-redis在哪里使用了Redis实现分布式会话共享?

DefaultSessionManger中的SessionDAO用于session persistence,如果不设置,默认的是MemeorySessionDAO(ConcurrentHashMap),用内存持久化session(无法解决分布式服务器会话共享)

而在这个项目中,使用的是用redis持久化session

subject.login(token):

token是用户提交的东西,一般有两部分Principal(账号或者用户名)和Credentials(密码或者验证的东西)。用户提交了token,则会进入自定义的Realm的doGetAuthenticationInfo()中获取我们返回的SimpleAuthenticationInfo。此时token包含principal与credentials,simpleAuthenticationInfo同样包含principal与credentials,区别就在于:token中的principal与credentials由用户决定,simpleAuthentication中的principal是由业务决定,最关键的credentials是根据token中的principal得到(可能是查session也可能是查数据库)。之后shiro便开始校验:校验用户给出的token中的credentials是否与simpleAuthentication中的credentials一致。

至于分布式会话共享:

需求在于:当用户已经获取到jwt,并且已经登录成功一次的时候,要通过session记录其已登陆状态。当再次发起请求的时候,不需要再次登录了,因为session中已有他的登录记录。但由于分布式服务器的存在,每次用户的请求可能不会委派给同一台服务器,便需要会话共享。多台服务器之间共享统一session记录,redis可以实现这点。每次用户登陆成功以后,shiro创建session,并生成JSESSIONID与session一一对应,一方面将JSESSIONID-session存入redis,一方面将JESSIONID存入cookie返回给客户端。由此,当用户再次发起请求并携带cookie的时候,shiro会根据cookie中的JESSIONID查询redis是否有该key,有的话则直接不需要再登录,没有的话就进行传统doGetAuthenticationInfo()。部署redis的服务器要允许部署项目的服务器访问redis,可以这些服务器通内网(最安全),也可以部署redis的服务器设置bind为0.0.0.0(暴露在公网上),并设置redis密码,同时可以配置安全组。

image.png

image.png

关于JSESSIONID:

image.png

image.png

Shiro各个配置:

1.自定义Realm:(关于角色与权限后期再开发,先实现认证功能)

注意:Shiro中使用自定义token时,要重写下面方法:

  @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

完整代码:

@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserService userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权(认证成功后授予权限)
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 认证(认证帐号密码)
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        JwtToken jwtToken=(JwtToken) authenticationToken;
        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();//shiro收到的是token,token携带jwt,解析得到id
        User user = userService.getById(Long.valueOf(userId));
        if(user==null){
            throw new UnknownAccountException("账户不存在");
        }
        if(user.getStatus()==-1){
            throw new LockedAccountException("账户已被锁定");
        }

        AccountProfile profile=new AccountProfile();
        BeanUtils.copyProperties(user,profile);

        return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());
    }
}

2.自定义JwtFilter

实现createToken():在请求经过过滤器的时候,生成token并返回

实现onAccessDenied():处理放行还是拦截的逻辑

重写onLoginFailure():为了返回给前端统一结果类Result

实现preHandle():对跨域提供支持

自定义Token:principal与credential都是token本身

public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String jwt){
        this.token=jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

完整代码:

@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    /**
     * 生成token
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request=(HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(jwt==null||jwt.length()==0){//没有获取到jwt
            return null;
        }
        return new JwtToken(jwt);
    }

    /**
     * 拦截,判断有无jwt
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request=(HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(jwt==null||jwt.length()==0){
            return true;//放行,比如游客,就不携带jwt
        }else{
            //校验jwt
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim==null || jwtUtils.isTokenExpired(claim.getExpiration())){ //null表示出异常,expired表示已过期
                throw new ExpiredCredentialsException("token已失效,请重新登录");
            }
            //执行登录
            return executeLogin(servletRequest,servletResponse);
        }
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse=(HttpServletResponse)response;
        Throwable throwable= e.getCause()==null?e:e.getCause();
        Result result = Result.fail(throwable.getMessage());
        String json = JSONUtils.toJSONString(result);//因为是过滤器返回结果,不是Controller里有@ResponseBody注解,所以需要手动转json并利用输出流返回给前端
        try {
            httpServletResponse.getWriter().write(json);  //TODO httpServletResponse.getWriter().print(json)与write有什么区别
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

3. ShiroConfig

配置RedisManager:配置redis

配置SessionManager: 配置Shiro的session持久化

配置SessionSecuiryManager:配置shiro的自定义认证逻辑;Cache的作用是把每次通过Realm查询到的结果存入Session,以便用户访问时加快Authentication和Authorization。如果要使用cache,必须在 securityManager 和 sessionManager 中同时进行配置。(所以cacheManager作用是生成Session?sessionManager的作用是将生成的Session持久化到redis中?)

配置ShiroFilterFactoryBean: 添加自定义过滤器并取名字;添加拦截链到过滤器工厂。

配置ShiroFilterChainDefinition: 配置拦截链(各个过滤器所负责过滤的请求路径)


@Configuration
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private int database;

    /**
     * redis的控制器,控制shiro-redis-spring-boot-starter连接的redis,默认是localhost:6379
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setDatabase(database);
        redisManager.setPassword(password);
        return redisManager;
    }

    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
    
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // inject redisSessionDAO
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

    @Bean
    public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        //inject sessionManager
        securityManager.setSessionManager(sessionManager);

        // inject redisCacheManager
        securityManager.setCacheManager(redisCacheManager);

        return securityManager;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限  //filterMap.put("/**", "authc"); authc是默认的,所有请求都需要登陆后才能访问
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition,
                                                         JwtFilter jwtFilter) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();//此时所有的请求都会经过jwtFilter
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

}

一些图解总结

627F902B69B477B6E46952A4FA5DB947.png

0F4F600AA3F1F21D5CC40F1DFAA29026.png

C67B005E2187C5A76FF86B59DAEE3E8C.png

BB7D2973B6C0F247789C171879EB36BE.png

BE1703C7982B390C1DCDE444486552E5.png