SpringBoot集成Shiro权限+Jwt认证

摘要:本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。

背景

  • 为什么要使用Shiro

随大流吧,虽然自己也可以基于自定义注解+拦截器实现和Shiro一样的功能,但是为了适用于业界的规范,所以集成这个大家都能看得懂,而且Shiro也相对简单。

  • 为什么要用Jwt

传统的session模式越来越少,而且大多数系统都是微服务多客户端的,所以无状态的登陆更符合现阶段的业务架构。

开始

本案例基于SpringBoot 2.5.X + Shiro 1.8 + hutool的Jwt

pom.xml

xml 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.24</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
    </dependencies>

ResponseMessage返回消息体

typescript 复制代码
@Data
public class ResponseMessage<T> {

    private Boolean success = Boolean.TRUE;

    private String code;

    private String message;

    private T data;

    public static <T> ResponseMessage<T> success(T data){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setCode("200");
        responseMessage.setMessage("操作成功");
        responseMessage.setData(data);
        return responseMessage;
    }

    public static <T> ResponseMessage<T> fail(String message){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setSuccess(Boolean.FALSE);
        responseMessage.setCode("500");
        responseMessage.setMessage(message);
        return responseMessage;
    }

}

GlobalExceptionHandler全局异常处理

less 复制代码
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseMessage<String> handleAllExceptions(Exception ex, WebRequest request) {
        // 处理异常
        log.error("业务异常",ex);
        return ResponseMessage.fail(ex.getMessage());
    }

}

JwtUtils工具类

typescript 复制代码
public class JwtUtils {

    private static final byte[] KEY = "ABADEXU".getBytes();

    public static String createToken(Map<String, Object> payload){
        return JWTUtil.createToken(payload, JwtUtils.KEY);
    }

    public static JWT parseToken(String token){
        return JWTUtil.parseToken(token);
    }

    public static Boolean verify(String token){
        return JWTUtil.verify(token, JwtUtils.KEY);
    }
}

JwtToken 认证dto

typescript 复制代码
@Data
public class JwtToken implements AuthenticationToken {

    /** JWT 认证串 */
    private String jwt;

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

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

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

}

JwtFilter 权限认证过滤器

拦截请求接口的

scala 复制代码
@Slf4j
public class JwtFilter extends AccessControlFilter {


    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest httpServletRequest   = (HttpServletRequest) request;
        String jwt                              = httpServletRequest.getHeader("Authorization");
        if(StrUtil.isNotBlank(jwt)){
            getSubject(request, response).login(new JwtToken(jwt));
            return true;
        }
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        throw new RuntimeException("身份验证异常");
    }

}

JwtRealm 授权领域

授权流程JwtFilter#isAccessAllowed -> JwtRealm#supports -> JwtRealm#doGetAuthenticationInfo -> JwtRealm#doGetAuthorizationInfo

java 复制代码
@Slf4j
public class JwtRealm extends AuthorizingRealm {

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("验证jwt token 权限");
        String jwt = principalCollection.getPrimaryPrincipal().toString();
        // 这里一般就从redis中拿用户的权限信息,案例就直接写死了
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 设置角色
        simpleAuthorizationInfo.addRole("admin");
        simpleAuthorizationInfo.addRole("user");
        // 设置权限
        simpleAuthorizationInfo.addStringPermission("user:add");
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("验证jwt token 有效性");
        String jwt = authenticationToken.getPrincipal().toString();
        // 从redis查询jwt token是否还存在,是否有效
        if(!Boolean.TRUE.equals(JwtUtils.verify(jwt))){
            throw new RuntimeException("jwt token 失效");
        }
        JWT parseToken = JwtUtils.parseToken(jwt);
        Object expiryTime = parseToken.getPayload("expiryTime");
        // 验证token是否过期
        return new SimpleAuthenticationInfo(jwt, jwt, this.getClass().getName());
    }
}

ShiroConfig 配置

typescript 复制代码
@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(jwtRealm());
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(defaultWebSecurityManager());
        // 未授权跳转
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        Map<String, Filter> filterMap = new HashMap<>();
        //
        filterMap.put("jwt", new JwtFilter());
        //
        shiroFilter.setFilters(filterMap);

        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        // 匿名访问
        filterRuleMap.put("/error", "anon");
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/logout", "anon");
        filterRuleMap.put("/unauthorized", "anon");
        // 登录并具有 admin 角色
        // filterRuleMap.put("/index/admin", "authc,roles[admin]");
        // filterRuleMap.put("/index/admin", "jwt,roles[admin]");
        // 通过jwt校验,需登录才能访问(自行实现逻辑)
        filterRuleMap.put("/**", "jwt");
        //
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
        //
        return shiroFilter;
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setUsePrefix(true);
        return advisorAutoProxyCreator;
    }

}

测试相关

LoginController 登陆接口
typescript 复制代码
@RestController
public class LoginController {

    @GetMapping("login")
    public Object login(String username){
        Map<String, Object> payload = new HashMap<>();
        payload.put("username", username);
        // 设置30分钟后过期
        payload.put("expiryTime", DateUtil.date().offset(DateField.MINUTE, 30));
        return JwtUtils.createToken(payload);
    }

}
UserController 权限验证接口
less 复制代码
@Slf4j
@RestController
@RequestMapping(value = "user")
public class UserController {

    @RequiresPermissions(value = {"user:view"})
    @GetMapping(value = "page")
    public Object page(){
        log.info("page");
        return "SUCCESS";
    }

    @RequiresPermissions(value = {"user:add"})
    @GetMapping(value = "add")
    public Object add(){
        log.info("add");
        return "SUCCESS";
    }

}
相关推荐
贫民窟的勇敢爷们7 小时前
SpringBoot整合AOP切面编程实战,实现日志统一记录+接口权限校验
java·spring boot·spring
吾疾唯君医11 小时前
Java SpringBoot集成积木报表实操记录
java·spring boot·spring·导出excel·积木报表·数据文件下载
正儿八经的少年14 小时前
Spring Boot 两种激活配置方式的作用与区别
java·spring boot·后端
疯狂成瘾者14 小时前
Spring Boot 项目中的 SMTP 邮件验证码服务技术解析
java·spring boot·后端
啃臭16 小时前
AOP和反射
java·spring boot
河阿里16 小时前
SpringBoot:Spring Task定时任务完整使用教学
java·spring boot·spring
五阿哥永琪19 小时前
从0开始做一个导出功能,完整流程
spring boot
java1234_小锋20 小时前
SpringBoot可以同时处理多少请求?
java·spring boot·后端
海棠Flower未眠20 小时前
Spring Boot 3 + JPA多模块系统对MySQL和DORIS进行多数据源集成实战(荣耀典藏版)
spring boot·后端·mysql
北风朝向21 小时前
Spring Boot 集成 Open WebUI 实现 AI 流式对话
人工智能·spring boot·状态模式