Springboot整合Shiro+JWT

发布时间:2023-12-31 09:30

在前后端分离的项目中我们通常会采用jwttoken的方式来作为跨域身份验证解决方案。

引入依赖

<dependency>
    <groupId>org.crazycakegroupId>
    <artifactId>shiro-redis-spring-boot-starterartifactId>
    <version>3.2.1version>
dependency>


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

在配置文件中配置我们的jwt信息

petrichor:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效时长,7天,单位秒
    expire: 604800
    header: token

准备一个jwt工具类,用来生成或解析jwt

@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "petrichor.jwt")// 绑定配置文件中的配置
public class JwtUtils {

    private String secret;// 盐
    private long expire;// 有效时长
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 解析
    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.debug("validate is token error ", e);
            return null;
        }
    }

    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

准备一个JwtToken类用来将生成的jwt保存在token中

// 将JWT保存在token中,shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken
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;
    }
}

jwt过滤器

因为 JWT 的整合,我们需要自定义自己的过滤器 JWTFilter

@Component
// 继承的是Shiro内置的AuthenticatingFilter,内置了可以自动登录方法的过滤器
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    // 实现登录,生成自定义支持的JwtToken
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");// 从请求头中拿到JWT
        if(StringUtils.isEmpty(jwt)) {// 判断JWT是否为空
            return null;
        }

        return new JwtToken(jwt);// 不为空则生成token
    }

    @Override
    // 拦截校验
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)) {// 当这个请求没有JWT时,就不需要交给shiro做登录处理,直接交给注解进行拦截,可以访问一些无需登录就可以访问的接口
            return true;
        } else {

            // 校验jwt, jwt用Claims对象来存自定义的信息
            Claims claim = jwtUtils.getClaimByToken(jwt);// 解析
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                throw new ExpiredCredentialsException("token已失效,请重新登录");
            }

            // JWT没问题就可以交给shiro执行登录
            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 = JSONUtil.toJsonStr(result);// 将对象转化未json

        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {

        }
        return false;
    }
}

代码中的Result类是我们自己封装的一个结果集

@Data
// 统一结果封装
public class Result implements Serializable {
    private int code; // 200是正常,非200表示异常
    private String msg;
    private Object data;

    public static Result succ(Object data) {
        return succ(200, "操作成功", data);
    }

    public static Result succ(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

    public static Result fail(String msg) {
        return fail(400, msg, null);
    }

    public static Result fail(String msg, Object data) {
        return fail(400, msg, data);
    }

    public static Result fail(int code, String msg, Object data) {
        Result r = new Result();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }
}

Realm类

@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserService userService;

    @Override
    // 判断这个token是否是JwtToken,让realm支持jwt的凭证校验
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    // 授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    // 认证
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        JwtToken jwtToken = (JwtToken) token;// 前面supports方法已经验证过这个token是JwtToken

        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();

        User user = userService.getById(Long.valueOf(userId));
        if (user == null) {
            throw new UnknownAccountException("账户不存在");
        }

        if (user.getStatus() == -1) {
            throw new LockedAccountException("账户已被锁定");
        }

        AccountProfile profile = new AccountProfile();// 存储用户信息的载体
        BeanUtil.copyProperties(user, profile);// 将user中的用户信息复制到profile

        return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
    }
}
@Data
// 登录成功之后返回的一个用户信息的载体
public class AccountProfile implements Serializable {

    private Long id;

    private String username;

    private String avatar;

    private String email;

}

ShiroConfig 配置类

如果考虑到我们的项目后面需要做服务集群与负载均衡,我们就需要将Redis顺带配置进来,shiro的缓存和会话信息我们都存储在Redis当中。所以当集群中的服务需要获得缓存和会话信息数据时,就可以从redis中获取。这样就能实现会话共享。

@Configuration
public class ShiroConfig {

    @Autowired
    JwtFilter jwtFilter;

    @Bean
    // shiro整合redis,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // 引入 redisSessionDAO
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

    @Bean
    // shiro整合redis,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        //引入 sessionManager
        securityManager.setSessionManager(sessionManager);

        // 引入 redisCacheManager
        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        Map<String, String> filterMap = new LinkedHashMap<>();

        filterMap.put("/**", "jwt");// 拦截,需要经过jwt过滤器才可以
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);// 注入jwt过滤器
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();// 获取shiroFilterChainDefinition方法中设置的拦截

        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

}

跨域处理

在前后端分离项目中,因为这时候前端和后端的代码是在不同机器上运行的,两个地址不在一个域名下,这个时候前端脚本在进行ajax访问的时候浏览器就会报跨域相关的错误。跨域请求会被浏览器的同源策略限制,所以我们要在服务器配置跨域处理。

在JwtFilter类中添加

@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);
}

跨域配置

/**
 * 解决跨域问题
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号