由于官方的shiro-spring-boot-web-starter
提供的功能过于简单,因此这里又造了一个加强版的轮子。
对比官方的starter,shiro-starter:
- 是在官方starter的基础上扩展的,兼容官方starter
- 将更多的参数配置化,使用起来更方便、灵活
- 提供两种运行模式
SESSION模式
[默认]:与Shiro官方提供的starter无二STATELESS模式
:无状态模式,也是现在许多大型系统喜欢采用的认证模式
- 提供了一些常见的认证方案支撑(如JWT),通过简单配置即可集成
- spring-boot:
1.5.9.RELEASE
- shiro-spring:
1.4.0-RC2
该starter没有刻意针对Spring Boot 2.x
做兼容,不过就实际使用反馈来说,在2.x环境下也暂时并没有出现什么兼容性问题,因此2.x用户也可以放心使用。
<dependency>
<groupId>com.github.taccisum</groupId>
<artifactId>shiro-starter</artifactId>
<version>{version}</version>
</dependency>
- 配置application.yml
shiro:
filter-chain-definition:
anon:
- /login
- /logout
authc:
- /**
- 配置realm
@Bean
public Realm realm() {
SimpleAccountRealm realm = new SimpleAccountRealm("test_realm");
realm.addAccount("tac", "123456", "staff");
return realm;
}
注意:在Stateless
模式下,shiro认证是在filter中进行的,因此其中抛出的异常不会被spring MVC的异常处理器捕获,而是会重定向到error页面。
- 配置application.yml
shiro:
web:
mode: stateless
filter-chain-definition:
anon:
- /login
authn:
- /goods/recommendations
authc:
- /**
- 指定realm
@Bean
public Realm realm() {
SimpleHashRealm realm = new SimpleHashRealm();
realm.setCredentialsMatcher(new StatelessCredentialsMatcher()); //这行不能少,否则在token模式下凭证校验会不通过
realm.addAccount("cd6765734a16476b9bd4b0513b3fb8e4", "staff");
return realm;
}
然后通过header
或parameter
传入token={hash},StatelessUserFilter
会自动调用subject.login(token)
为每一次无状态请求进行认证。
- 添加JWT依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
shiro-starter检测到存在该依赖时会自动创建JWTManager
及相关bean供realm使用。
- 指定realm
@Bean
public Realm realm(JWTManager jwtManager) {
SimpleJWTRealm realm = new SimpleJWTRealm(jwtManager);
realm.setCredentialsMatcher(new StatelessCredentialsMatcher());
return realm;
}
然后你可以:
- 创建JWT
@Autowired
private JWTManager jwtManager;
public String login() {
Payload payload = new Payload();
payload.put("uid", 123L);
payload.put("username", "tac");
payload.put("roles", "STAFF,DEVELOPER");
payload.put("permissions", "system:user:view,system:user:add");
// 支持自定义对象,List,Map等类型
payload.put("map", map);
payload.put("list", list);
// 自定义对象
payload.put("entity", entity);
return jwtManager.create("access_token", payload);
}
- 解析token为payload
@Autowired
private JWTManager jwtManager;
public Payload parseJWT() {
return jwtManager.verifyAndParsePayload("access_token", SecurityUtils.getSubject().getPrincipal().toString());
}
- 指定payload格式
可以通过PayloadTemplate
自由指定JWT payload的格式。
JWTManager
在创建JWT时会根据JWT的issuer
选取对应的PayloadTemplate
校验payload是否合法。同时,在将JWT解析成payload时也会根据PayloadTemplate
确定有哪些字段。
@Bean
public PayloadTemplate payloadTemplate() {
PayloadTemplate payloadTemplate = new DefaultPayloadTemplate("access_token");
payloadTemplate.addField("uid", Long.class);
payloadTemplate.addField("uname", String.class);
payloadTemplate.addField("isAdmin", Boolean.class);
payloadTemplate.addField("type", String.class);
payloadTemplate.addField("roles", String.class);
payloadTemplate.addField("list", List.class);
payloadTemplate.addField("map", Map.class);
payloadTemplate.addField("entity", Entity.class); // Entity为自定义类型
return payloadTemplate;
}
PayloadTemplate
可以创建多个,但每一个的issuer
必须不同。当没有定义任何template时,会提供一个默认的template[issuer: access_token]:uid, username, roles, permissions。
- 指定加密算法
可以通过JWTAlgorithmProvider
指定JWT的加密算法。默认为HMAC256
。
@Bean
protected JWTAlgorithmProvider jwtAlgorithmProvider() {
return new DefaultJWTAlgorithmProvider();
}
SimpleJWTRealm
不支持登出操作,每个JWT都有固定的有效时间,无法强制使其失效- 默认的
JWTAlgorithmProvider
将在每次应用启动时生成一个UUID作为密钥,因此在应用重启后,此前生成的JWT会全部失效
通过覆写ShiroFilterDefinition
bean即可实现
@Bean
public ShiroFilterDefinition shiroFilterDefinition() {
return filters -> {
logger.info("replace [authc] filter by " + StatelessUserFilter.class);
filters.put("authc", new StatelessUserFilter(shiroWebProperties));
// add more filters
};
}
- authn: 仅当用户请求提供了 token 时才执行认证逻辑,未提供时将直接放行(视为匿名用户),适用于一些认证前后均可访问,且业务逻辑会有所不同的 api(例如商品推荐)
这部分的封装是针对微服务架构中,由于系统间的调用往往是默认可信的,所以省去了 token 的验证过程,直接解析其中的 payload 供业务代码使用
指定 Realm
@Bean
public Realm realm(String issuer, JWTManager jwtManager) {
OnlyParseJWTRealm realm = new OnlyParseJWTRealm(issuer, jwtManager);
realm.setCredentialsMatcher(new StatelessCredentialsMatcher());
return realm;
}
你可以通过提供 TokenExtractor
的 bean 来修改从 http 请求中获取 token 的逻辑,shiro-starter
默认提供了 PowerTkExtractor
供使用,如下
@Bean
public TokenExtractor tokenExtractor(){
PowerTkExtractor bean = new PowerTkExtractor("token", "tk", "auth");
if (mock) {
bean.setMock(true);
bean.setMockTk("mock_tk");
}
return bean;
}
具体用法请参数 PowerTkExtractor
的 Javadoc。
properties | 描述 | 默认值 | 适用模式 |
---|---|---|---|
shiro.web.mode | 指定shiro启动模式 | SESSION | ALL |
shiro.web.redirect-enabled | 是否允许shiro重定向页面 | true | ALL |
shiro.web.filter-chain-definition | 定义shiro filter chain | {} | ALL |
shiro.loginUrl | 用户未认证时重定向的页面 | /login.jsp | ALL |
shiro.successUrl | 用户认证成功后跳转的默认页面 | / | ALL |
shiro.unauthorizedUrl | 用户未授权时的重定向页面(403) | null | ALL |
shiro.sessionManager.deleteInvalidSessions | 移除无效session | true | SESSION |
shiro.sessionManager.sessionIdCookieEnabled | 启用cookie session ID | true | SESSION |
shiro.sessionManager.sessionIdUrlRewritingEnabled | 启用session URL重写支持 | true | SESSION |
shiro.userNativeSessionManager | 使用原生的session manager | false | SESSION |
shiro.sessionManager.cookie.name | session cookie名称 | JSESSIONID | SESSION |
shiro.sessionManager.cookie.maxAge | session cookie最大生存时间 | -1 | SESSION |
shiro.sessionManager.cookie.domain | session cookie domain | null | SESSION |
shiro.sessionManager.cookie.path | session cookie path | null | SESSION |
shiro.sessionManager.cookie.secure | session cookie secure flag | false | SESSION |
shiro.rememberMeManager.cookie.name | RememberMe cookie名称 | JSESSIONID | SESSION |
shiro.rememberMeManager.cookie.maxAge | RememberMe cookie最大生存时间 | -1 | SESSION |
shiro.rememberMeManager.cookie.domain | RememberMe cookie domain | null | SESSION |
shiro.rememberMeManager.cookie.path | RememberMe cookie path | null | SESSION |
shiro.rememberMeManager.cookie.secure | RememberMe cookie secure flag | false | SESSION |
- 支持OAuth2.0集成
- 提供基于Redis存储的JWT Realm