SpringBoot+Shiro+JWT实现「登录认证」和「权限校验」
2025-06-24 12:14:44
来源:新华网
前言
在我初学登录模块的时候,登录认证是通过JWT和拦截器实现的。最近在写项目时遇到接口需要权限验证,我打算用一个安全框架来实现。经过我的考察最终在SpringSecurity和Shiro中选择了Shiro,前者我感觉要复杂一些,对于我这个小项目Shiro就足够了。研究了一下,终于写出了demo。代码可以直接跑,我省去了数据库等一些操作。再次之前你需要了解「Shiro的认证流程」和「RABC权限模型」。
完整代码:https://github.com/zazhiii/springboot-shiro-jwt
0. 项目结构
com.zazhi.shiro_demo│── common│ ├── Result│ ├── JwtUtil││── controller│ ├── MyController││── pojo│ ├── User││── service│ ├── UserService││── shiro│ ├── ShiroConfig│ ├── JwtFilter│ ├── AccountRealm│ ├── JwtToken│ ├── GlobalExceptionHandler││── ShiroDemoApplication│resources│── application.yml
common
:通用工具类(JwtUtil
)和返回结果封装(Result
)。controller
:MyController
处理请求。service
:业务逻辑层(UserService
)。- shiro:Shiro 相关配置,包括
ShiroConfig
、JwtFilter
、AccountRealm
、JwtToken
以及全局异常处理GlobalExceptionHandler
。 resources
:配置文件application.yml
。
1. 导入依赖
<dependencies><dependency><groupId>commons-logginggroupId><artifactId>commons-loggingartifactId><version>1.2version>dependency><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-webartifactId>dependency><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-testartifactId>dependency><dependency><groupId>org.apache.shirogroupId><artifactId>shiro-spring-boot-starterartifactId><classifier>jakartaclassifier><version>2.0.1version><exclusions><exclusion><groupId>org.apache.shirogroupId><artifactId>shiro-crypto-cipherartifactId>exclusion><exclusion><groupId>org.apache.shirogroupId><artifactId>shiro-crypto-hashartifactId>exclusion><exclusion><groupId>org.apache.shirogroupId><artifactId>shiro-webartifactId>exclusion><exclusion><groupId>org.apache.shirogroupId><artifactId>shiro-springartifactId>exclusion>exclusions>dependency><dependency><groupId>org.apache.shirogroupId><artifactId>shiro-webartifactId><classifier>jakartaclassifier><version>2.0.1version>dependency><dependency><groupId>org.apache.shirogroupId><artifactId>shiro-springartifactId><classifier>jakartaclassifier><version>2.0.1version>dependency><dependency><groupId>com.auth0groupId><artifactId>java-jwtartifactId><version>4.4.0version>dependency><dependency><groupId>com.github.xiaoymingroupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starterartifactId><version>4.4.0version>dependency><dependency><groupId>org.projectlombokgroupId><artifactId>lombokartifactId><version>1.18.32version>dependency>dependencies>
其中的shiro依赖的导入我参考别人的文章,这样可以兼容SpringBoot 3.x,具体原理我也不懂。
2. 添加Shiro配置类
注:他们的作用在代码注释中做了简单解释。
importjakarta.servlet.Filter;importorg.apache.shiro.mgt.DefaultSessionStorageEvaluator;importorg.apache.shiro.mgt.DefaultSubjectDAO;importorg.apache.shiro.spring.web.ShiroFilterFactoryBean;importorg.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;importorg.apache.shiro.spring.web.config.ShiroFilterChainDefinition;importorg.apache.shiro.web.mgt.DefaultWebSecurityManager;importorg.springframework.boot.web.servlet.FilterRegistrationBean;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.HashMap;importjava.util.Map;@ConfigurationpublicclassShiroConfig{ @BeanpublicShiroFilterFactoryBeanshiroFilterFactoryBean(DefaultWebSecurityManagersecurityManager,ShiroFilterChainDefinitionshiroFilterChainDefinition,JwtFilterjwtFilter){ // ShiroFilterFactoryBean 用于配置 Shiro 的拦截器链,并与 SecurityManager 关联。ShiroFilterFactoryBeanshiroFilterFactoryBean =newShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 并将 JwtFilter 实例与之("jwt")关联。相当于给这个拦截器起了个名字// JwtFilter 负责处理所有请求的 JWT 认证。Map<String,Filter>filters =newHashMap<>();filters.put("jwt",jwtFilter);shiroFilterFactoryBean.setFilters(filters);// setFilterChainDefinitionMap 用来定义 URL 路径与过滤器的映射关系。所有请求都会通过 jwt 过滤器进行身份验证。shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());returnshiroFilterFactoryBean;}// shiroFilterChainDefinition 定义了 URL 路径与过滤器的映射规则。// 在这个例子中,所有的请求 (/**) 都必须通过 jwt 过滤器进行身份验证// 如果有其他请求需要不同的权限控制,可以在这个方法中进一步调整或添加不同的过滤规则。@BeanpublicShiroFilterChainDefinitionshiroFilterChainDefinition(){ DefaultShiroFilterChainDefinitionchainDefinition =newDefaultShiroFilterChainDefinition();chainDefinition.addPathDefinition("/**","jwt");returnchainDefinition;}// 创建 SecurityManager 对象,并设置自定义的 AccountRealm 作为认证器。@BeanpublicDefaultWebSecurityManagerdefaultWebSecurityManager(AccountRealmaccountRealm){ DefaultWebSecurityManagerdefaultWebSecurityManager =newDefaultWebSecurityManager(accountRealm);// 关闭sessionDefaultSubjectDAOdefaultSubjectDAO =newDefaultSubjectDAO();DefaultSessionStorageEvaluatorsessionStorageEvaluator =newDefaultSessionStorageEvaluator();sessionStorageEvaluator.setSessionStorageEnabled(false);defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);defaultWebSecurityManager.setSubjectDAO(defaultSubjectDAO);returndefaultWebSecurityManager;}// 防止 Spring 将 JwtFilter 注册为全局过滤器// 没有这个的话请求会被 JwtFilter 拦截两次@BeanpublicFilterRegistrationBean<Filter>registration(JwtFilterfilter){ FilterRegistrationBean<Filter>registration =newFilterRegistrationBean<Filter>(filter);registration.setEnabled(false);returnregistration;}}
3. 添加自定义Realm
Realm类的作用可以参考其他文章,有很多讲得不错的文章,这里不赘述了。
importcom.zazhi.shiro_demo.common.JwtUtil;importcom.zazhi.shiro_demo.service.UserService;importlombok.extern.slf4j.Slf4j;importorg.apache.shiro.authc.AuthenticationException;importorg.apache.shiro.authc.AuthenticationInfo;importorg.apache.shiro.authc.AuthenticationToken;importorg.apache.shiro.authc.SimpleAuthenticationInfo;importorg.apache.shiro.authz.AuthorizationInfo;importorg.apache.shiro.authz.SimpleAuthorizationInfo;importorg.apache.shiro.realm.AuthorizingRealm;importorg.apache.shiro.subject.PrincipalCollection;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjava.util.Map;importjava.util.Set;@Slf4j@ComponentpublicclassAccountRealmextendsAuthorizingRealm{ @AutowiredprivateUserServiceuserService;// 这个方法用于判断 AccountRealm 是否支持该类型的 Token。@Overridepublicbooleansupports(AuthenticationTokentoken){ returntoken instanceofJwtToken;}// 用于授权@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipals){ Stringusername =(String)principals.getPrimaryPrincipal();// 这里查询数据库获取用户的角色和权限// TODO: 这个例子中,我们模拟了从数据库中查询用户的角色和权限信息。// 实际项目中,你需要根据业务逻辑从数据库中查询用户的角色和权限信息。Set<String>roles =userService.findRolesByUsername(username);// 示例:{ admin}Set<String>permissions =userService.findPermissionsByUsername(username);// 示例:{ "user:delete", "user:update"}// 并使用 addRoles 和 addStringPermissions 方法将角色和权限添加到授权信息中。SimpleAuthorizationInfoauthorizationInfo =newSimpleAuthorizationInfo();authorizationInfo.setRoles(roles);authorizationInfo.setStringPermissions(permissions);returnauthorizationInfo;}// 用于验证用户身份@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokenauthenticationToken){ JwtTokentoken =(JwtToken)authenticationToken;StringjwtToken =(String)token.getPrincipal();// 这里是真正验证 JwtToken 是否有效的地方// 如果验证失败,抛出 AuthenticationException 异常。这个请求会被认定为未认证请求。后续返回给前端Map<String,Object>map;try{ map =JwtUtil.parseToken(jwtToken);}catch(Exceptione){ thrownewAuthenticationException("该token非法,可能被篡改或过期");}Stringusername =(String)map.get("username");// TODO:这里可以根据业务逻辑自定义验证逻辑// 例如:1.根据用户名查询数据库,判断用户是否存在 2.判断用户状态是否被锁定等returnnewSimpleAuthenticationInfo(username,jwtToken,getName());}}
4. 实现JwtFilter类和JwtToken类
JwtFilter
importjakarta.servlet.ServletRequest;importjakarta.servlet.ServletResponse;importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importlombok.extern.slf4j.Slf4j;importorg.apache.shiro.authc.AuthenticationException;importorg.apache.shiro.authc.AuthenticationToken;importorg.apache.shiro.web.filter.authc.AuthenticatingFilter;importorg.springframework.http.HttpStatus;importorg.springframework.stereotype.Component;importorg.springframework.util.StringUtils;importorg.springframework.web.bind.annotation.RequestMethod;@Slf4j@ComponentpublicclassJwtFilterextendsAuthenticatingFilter{ // 拦截请求之后,用于把令牌字符串封装成令牌对象// 该方法用于从请求中获取 JWT 并将其封装为 JwtToken(自定义的 AuthenticationToken 类)。@OverrideprotectedAuthenticationTokencreateToken(ServletRequestrequest,ServletResponseresponse){ HttpServletRequesthttpRequest =(HttpServletRequest)request;StringjwtToken =httpRequest.getHeader("Authorization");if(!StringUtils.hasLength(jwtToken)){ returnnull;}returnnewJwtToken(jwtToken);}// 该方法的作用是判断当前请求是否被允许访问。它主要用于检查某些条件,决定是否允许访问或是否跳过认证过程// 他作用于 onAccessDenied 之前// 如果请求满足某些条件(例如,isAccessAllowed 返回 true),Shiro 会跳过后续的认证步骤,允许请求继续。// 如果返回 false,Shiro 会继续执行 onAccessDenied,进行认证或其他授权操作。@OverrideprotectedbooleanisAccessAllowed(ServletRequestrequest,ServletResponseresponse,ObjectmappedValue){ // 从请求头中获取 TokenHttpServletRequesthttpRequest =(HttpServletRequest)request;StringjwtToken =httpRequest.getHeader("Authorization");if(StringUtils.hasLength(jwtToken)){ // 若当前请求存在 Token,则执行登录操作try{ log.info("请求路径 { } 开始认证, token: { }",httpRequest.getRequestURI(),jwtToken);getSubject(request,response).login(newJwtToken(jwtToken));log.info("{ } 认证成功",httpRequest.getRequestURI());}catch(AuthenticationExceptione){ log.error("{ } 认证失败",httpRequest.getRequestURI());}}// 若当前请求不存在 Token,没有认证意愿,直接放行// 例如,登录接口或者游客可访问的接口不需要 Tokenreturntrue;}@OverrideprotectedbooleanonAccessDenied(ServletRequestservletRequest,ServletResponseservletResponse)throwsException{ returntrue;}@OverrideprotectedbooleanpreHandle(ServletRequestrequest,ServletResponseresponse)throwsException{ HttpServletRequestreq =(HttpServletRequest)request;HttpServletResponseres =(HttpServletResponse)response;res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));res.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");res.setHeader("Access-control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态if(req.getMethod().equals(RequestMethod.OPTIONS.name())){ res.setStatus(HttpStatus.OK.value());// 返回true则继续执行拦截链,返回false则中断后续拦截,直接返回,option请求显然无需继续判断,直接返回returnfalse;}returnsuper.preHandle(request,response);}}
JwtToken
importorg.apache.shiro.authc.AuthenticationToken;/** * @author zazhi * @date 2024/12/10 * @description: JwtToken */publicclassJwtTokenimplementsAuthenticationToken{ privateStringtoken;publicJwtToken(Stringtoken){ this.token =token;}@OverridepublicObjectgetPrincipal(){ returntoken;}@OverridepublicObjectgetCredentials(){ returntoken;}}
5. 实现Controller和Service
Controller
importcom.zazhi.shiro_demo.common.Result;importcom.zazhi.shiro_demo.service.UserService;importio.swagger.v3.oas.annotations.tags.Tag;importlombok.extern.slf4j.Slf4j;importorg.apache.shiro.authz.annotation.RequiresAuthentication;importorg.apache.shiro.authz.annotation.RequiresPermissions;importorg.apache.shiro.authz.annotation.RequiresRoles;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;/** * @author zazhi * @date 2024/12/9 * @description: */@Slf4j@RestController()@RequestMapping("/api")@Tag(name ="MyController",description ="用户接口")publicclassMyController{ @AutowiredUserServiceuserService;// 模拟登录@GetMapping("/login")publicStringlogin(Stringusername,Stringpassword){ returnuserService.login(username,password);}// 不需要认证就能访问@GetMapping("/public")publicResult<String>pub(){ log.info("调用 pub");returnResult.success("公共页面");}// 需要「认证」才能访问@RequiresAuthentication@GetMapping("/profile")publicResult<String>profile(){ returnResult.success("个人信息页面");}// 需要「认证」和「特定角色」才能访问@RequiresAuthentication@RequiresRoles("admin")@GetMapping("/dashboard")publicResult<String>dashboard(){ log.info("调用 dashboard");returnResult.success("控制面板页面");}// 需要「认证」和「特定权限」才能访问@RequiresAuthentication@RequiresPermissions("view:dashboard")@GetMapping("/viewDashboard")publicResult<String>viewDashboard(){ returnResult.success("查看控制面板页面");}}
Service
注:这里省略了从数据库获取数据,直接模拟了数据的返回。
importcom.zazhi.shiro_demo.common.JwtUtil;importorg.springframework.stereotype.Service;importjava.util.Map;importjava.util.Set;/** * @author zazhi * @date 2024/12/9 * @description: TODO */@ServicepublicclassUserService{ publicStringlogin(Stringusername,Stringpassword){ // 判断逻辑省略returnJwtUtil.genToken(Map.of("username",username));}publicSet<String>findPermissionsByUsername(Stringusername){ returnSet.of("user:delete","user:update");}publicSet<String>findRolesByUsername(Stringusername){ // 模拟从数据库中查询用户角色if(username.equals("admin")){ returnSet.of("admin");}returnSet.of("user");}}
6. 全局异常处理类
对于认证失败、鉴权失败的情况,我们用全局异常处理器捕获相应的异常返回对应信息给前端。
importcom.zazhi.shiro_demo.common.Result;importlombok.extern.slf4j.Slf4j;importorg.apache.shiro.authz.UnauthenticatedException;importorg.apache.shiro.authz.UnauthorizedException;importorg.springframework.http.HttpStatus;importorg.springframework.util.StringUtils;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.ResponseStatus;importorg.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{ /** * 处理未认证异常 * @param e * @return */@ResponseStatus(HttpStatus.UNAUTHORIZED)@ExceptionHandler(UnauthenticatedException.class)publicResulthandleUnauthenticatedException(UnauthenticatedExceptione){ returnResult.error("未认证或Token无效,请重新登录");}/** * 处理未授权异常 * @param e * @return */@ResponseStatus(HttpStatus.FORBIDDEN)@ExceptionHandler(UnauthorizedException.class)publicResulthandleUnauthorizedException(UnauthorizedExceptione){ returnResult.error("未授权");}/** * 处理其他异常 * @param e * @return */@ExceptionHandler(Exception.class)publicResulthandleException(Exceptione){ log.info("Exception: ",e);returnResult.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败");}}
7. 其他类
统一返回结果类
importlombok.Data;importjava.io.Serializable;/** * 后端统一返回结果 * @param */ @DatapublicclassResult<T>implementsSerializable{ privateIntegercode;//编码:1成功,0和其它数字为失败privateStringmsg;//错误信息privateTdata;//数据publicstatic<T>Result<T>success(){ Result<T>result =newResult<T>();result.code =1;returnresult;}publicstatic<T>Result<T>success(Tobject){ Result<T>result =newResult<T>();result.data =object;result.code =1;returnresult;}publicstatic<T>Result<T>error(Stringmsg){ Resultresult =newResult();result.msg =msg;result.code =0;returnresult;}}
JWT工具类
packagecom.zazhi.shiro_demo.common;importcom.auth0.jwt.JWT;importcom.auth0.jwt.algorithms.Algorithm;importjava.util.Date;importjava.util.Map;publicclassJwtUtil{ privatestaticfinalStringKEY="zazhi";//接收业务数据,生成token并返回publicstaticStringgenToken(Map<String,Object>claims){ returnJWT.create().withClaim("claims",claims).withExpiresAt(newDate(System.currentTimeMillis()+1000*60*60*24*7)).sign(Algorithm.HMAC256(KEY));}//接收token,验证token,并返回业务数据publicstaticMap<String,Object>parseToken(Stringtoken){ returnJWT.require(Algorithm.HMAC256(KEY)).build().verify(token).getClaim("claims").asMap();}// 验证token是否有效publicstaticbooleanverifyToken(Stringtoken){ try{ JWT.require(Algorithm.HMAC256(KEY)).build().verify(token);returntrue;}catch(Exceptione){ returnfalse;}}}
8. SpringBoot 启动!
在 application.yml 中配置一下knife4j
springdoc:swagger-ui:path:/swagger-ui.html tags-sorter:alpha operations-sorter:alpha api-docs:path:/v3/api-docs group-configs:-group:'default'paths-to-match:'/**'packages-to-scan:com.zazhi.shiro_demo.controller# knife4j的增强配置,不需要增强可以不配knife4j:enable:truesetting:language:zh_cn
启动!
在浏览器访问:localhost:8080/doc.html 就可以开始测试啦。