- Notifications
You must be signed in to change notification settings - Fork7
davidfantasy/shrio-with-jwt-spring-boot-starter
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
如需了解本框架的设计细节,请阅读:基于 Shiro 和 JWT 的无状态安全验证方案这篇文章
用户权限管理是每个信息系统最基本的需求,对基于 Java 的项目来说,最常用的权限管理框架就是大名鼎鼎的 Apache Shiro。Apache Shiro 功能非常强大,使用广泛,几乎成为了权限管理的代名词。但对于普通项目来说,Shiro 的设计理念因为追求灵活性,一些概念如 Realm,Subject 的抽象级别都比较高,显得比较复杂。如果没有对框架细节进行深入了解的话,很难理解其中的准确含义。要将其应用于实际项目,还需要针对项目的实际情况做大量的配置和改造,时间成本较高。
而且 Shiro 兴起的时代主流应用还是传统的基于 Session 的 Web 网站,并没有过多的考虑目前流行的微服务等应用形式的权限管理需求。导致其并没有提供一套无状态微服务的开箱即用的整合方案。需要在项目层面对 Shiro 进行二次封装和改进,开发难度较大。
shrio-with-jwt-spring-boot-starter 正是针对上述情况而开发的。它基于 spring-boot 环境,使用 Shiro 作为基础验证框架,整合了 JWT(JSON Web token)规范,通过简单的一些配置,提供在微服务环境下开箱即用的无状态权限管理框架。
- 完全兼容 Shiro
- 无状态设计,无需 Session
- 基于 JWT 规范的 Token 设计
- 在 spring-boot 环境下自动配置,开箱即用
- 基于注解的权限配置,并且兼容 Shiro 的层级权限设置
- 通过接口灵活定义获取用户权限(permission)的方式,兼容多种权限模型
- Token 过期前自动刷新(需配合客户端的实现)
- 引入 shrio-with-jwt-spring-boot-starter。
<dependency> <groupId>com.github.davidfantasy</groupId> <artifactId>shrio-with-jwt-spring-boot-starter</artifactId> <version>${version}</version></dependency>
- 根据实际业务的需要,实现com.github.davidfantasy.jwtshiro.JWTUserAuthService接口,并注册为Spring的bean(如果框架没有找到任何一个JWTUserAuthService的实现类,则不会进行任何处理)。JWTUserAuthService 接口是框架的一个扩展点,便于应用端根据自身的业务规则对权限模型,错误处理等进行自定义实现。getUserInfo方法用于客户端访问时根据客户端传回 token 中包含的用户 account 信息,获取用户的实际权限。获取的方式由应用程序端来控制,可以从配置文件中加载,也可以根据 account 查询数据库,获取用户实际权限。getAuthenticatedUser方法已提供默认实现,用于获取当前请求接口的客户信息,以下是一个例子
@ServicepublicclassJWTUserAuthServiceImplimplementsJWTUserAuthService {@AutowiredprivateUserServiceuserService;privateCache<String,UserInfo>userCache =CacheBuilder.newBuilder().maximumSize(1000) .expireAfterWrite(30,TimeUnit.MINUTES).build();@OverridepublicUserInfogetUserInfo(Stringaccount) {try {UserInfouser =userCache.getIfPresent(account);if (user ==null) {user =this.queryUserInfo(account);if (user !=null) {userCache.put(account,user); } }returnuser; }catch (Exceptione) {log.error("读取用户缓存信息发生错误:" +e.getMessage()); }returnnull; }/** * 自定义访问资源认证失败时的处理方式,例如返回json格式的错误信息 * {\"code\":401,\"message\":\"用户认证失败!\") */@OverridepublicvoidonAuthenticationFailed(HttpServletRequestreq,HttpServletResponseres) {res.setStatus(HttpStatus.UNAUTHORIZED.value()); }/** * 自定义访问资源权限不足时的处理方式,例如返回json格式的错误信息 * {\"code\":403,\"message\":\"permission denied!\") */@OverridepublicvoidonAuthorizationFailed(HttpServletRequestreq,HttpServletResponseres) {res.setStatus(HttpStatus.FORBIDDEN.value()); }privateShiroUserInfoqueryUserInfo(Stringaccount) {// 这里编写获取ShiroUserInfo的逻辑,例如从数据库进行查询 }/** * 调用接口的getAuthenticatedUser获取当前请求的用户信息 */publicShiroUserInfogetCurrentUser(){return (ShiroUserInfo)this.getAuthenticatedUser(false); }/** * 刷新指定account的缓存信息 */publicvoidrefreshUserCache(Stringaccount) {this.userCache.invalidate(account); }}
注意:getUserInfo 这个方法在每次接口调用的时候都会触发,用于检查用户权限,请实现时根据需要对接口的返回结果进行缓存(例如使用 Guava 的 Cache)。
返回值 com.github.davidfantasy.jwtshiro.UserInfo 类封装了一个系统用户必要的权限信息,可以根据实际需要进行扩展:
publicclassUserInfo {/** * 用户的唯一标识 */privateStringaccount;/** * accessToken的密钥,用于对accessToken进行加密和解密 * 建议为每个用户配置不同的密钥(比如使用用户的password) */privateStringsecret;/** * 用户权限集合,含义类似于Shiro中的perms */privateSet<String>permissions;}
- 对需要进行权限控制的 Controller 添加对应的注解,实现灵活的权限控制。**为了简化配置,框架默认所有被拦截的资源必须是要经过认证的用户才可以被访问。**即如果配置的拦截范围是/api/,则会添加一条默认的验证规则: /api/=authc。但任何通过注解添加的验证规则都拥有比默认规则更高的优先级。如果需要精确控制某个接口的用户权限,就需要利用到 RequiresPerms 和 AlowAnonymous 注解。添加了 AlowAnonymous 注解的 url 允许匿名访问,而 RequiresPerms 则用于指定某个 url 所需的用户权限,访问用户必须拥有该权限才允许访问该接口(用法和Shiro原生的@RequiresPermissions基本一致,不过是基于url进行拦截,不需要配置动态代理)。
注意:RequiresPerms 比 AlowAnonymous 拥有更高的优先级,如果一个 url 同时被设定了两种规则,则 AlowAnonymous 不会起作用。如果method和class同时添加了RequiresPerms注解,则method的注解拥有更高优先级。
下面是一个访问控制规则设置的例子:
@RestController@RequestMapping("/api/user")@RequiresPerms("user:basic")publicclassUserController {@AlowAnonymous@PostMapping("/login")publicStringlogin() {return"ok"; }@GetMapping("/detail")publicStringgetUserDetail() {return"ok"; }@PostMapping("/modify")@RequiresPerms("user:modify")publicStringmodifyUser() {return"ok"; }@PostMapping("/delete")@RequiresPerms({"system","user:delete"})publicStringdeleteUser() {return"ok"; }@PostMapping("/modify-logs")@RequiresPerms(value={"system","user:logs"},logical =Logical.OR)publicStringdeleteUser() {return"ok"; }}
在上面的例子中,接口与用户权限的对应关系如下:
接口 | 所需权限 |
---|---|
/api/user/login | 无需权限,可匿名访问 |
/api/user/detail | 访问用户需具备权限"user:basic" |
/api/user/modify | 访问用户需具备权限"user:modify" |
/api/user/delete | 访问用户需同时具备权限"system","user:delete" |
/api/user/modify-logs | 访问用户需具备权限"system"或者"user:logs" |
类似于 Shiro 官方的如下配置
<propertyname="filterChainDefinitions"> <value> /api/user/login = anon /api/user/detail = perms["user"] /api/user/modify = perms["user:modify"] /api/user/delete = perms["user:delete"] </value></property>
注意:和在 Shiro 中一样,权限是按层级划分的(使用:分割),即在上例中,如果用户拥有的权限中有“user”,则可以同时访问/api/user/detail,/api/user/modify,/api/user/delete 三个接口
客户端在访问非匿名接口前,都需要调用服务端的登录接口获取 accessToken,accessToken 有时效限制,在生命周期内由客户端负责对 accessToken 进行存储和管理。服务端的登录接口生成 accessToken 的示例代码如下:
@RestController@RequestMapping("/security")publicclassMockController {@AutowiredprivateMockUserServiceuserService;@AutowiredprivateJWTHelperjwtHelper;@AlowAnonymous@PostMapping("/login")publicResultlogin(Stringaccount,Stringpassword) {UserInfouser =userService.getUserInfo(account);if(user==null||!user.getPassword().equals(password)){thrownewIllegalArgumentException("用户名或密码错误"); }StringaccessToken =jwtHelper.sign(user.getAccount(),user.getPassword());//后续token的刷新由客服端负责维护Resultresult =newResult();result.setToken(accessToken);returnresult; }}
客户端登录后获取的 accessToken,每次调用接口时,都将 accessToken 加入到请求的 header 中供服务端进行权限验证。header 中的名称默认为"jwt-token",也可以通过配置修改为其它名称,请求示例如下:
accept: application/json, text/plain, */*accept-encoding: gzip, deflate, braccept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6jwt-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODQzNjG5OTtsImFjY291bnQiOiIxODcxNjYxODEzOCJ9.7eJYVmSys6YBu51Al5hdXdPMdrKsQFCqMwHu8ATaOPY
accessToken 的有效期由两个配置构成,maxAliveMinute 和 maxIdleMinute。maxAliveMinute 定义了 accessToken 的理论过期时间,而 maxIdleMinute 定义了accessToken 的最大生存周期。框架会自动注册一个 Spring 的 HandlerInterceptor 用来处理 Token 的自动刷新问题,如果传入的 Token 已经超过 maxAliveMinute 设定的时间,但还没有达到 maxIdleMinute 的限制,则会自动刷新该用户的 accessToken 并添加在 response header(header 中的名称取决于配置值),客户端如果在响应头中发现有新的 token 返回,说明当前 token 即将失效,需要及时更新自身存储的 token。
这个机制实际是提供一个窗口期,让客户端安全的刷新 accessToken。试想如果 token 失效了就必须立即重新登录,那势必会严重影响到用户的实际体验。
注意:要启用accessToken自动刷新机制,需配置enableAutoRefreshToken参数为true
参数名 | 默认值 | 说明 |
---|---|---|
jwt-shiro.urlPattern | /* | 需要进行权限拦截的 URL pattern, 多个使用 url 隔开,例如:/api/,/rest/ |
jwt-shiro.maxAliveMinute | 30 | accessToken 的理论过期时间,单位分钟,token 如果超过该时间则接口响应的 header 中附带新的 token 信息 |
jwt-shiro.maxIdleMinute | 60 | accessToken 的最大生存周期,单位分钟,在此时间内的 token 无需重新登录即可刷新 |
jwt-shiro.headerKeyOfToken | jwt-token | accessToken 在 http header 中的 name |
jwt-shiro.accountAlias | account | token 中保存的用户名的 key name |
jwt-shiro.enableAutoRefreshToken | false | 是否启用token自动刷新机制 |
About
spring-boot环境下基于JWT Token的无状态shiro权限验证框架
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.