SpringBoot+Shiro+JWT許可權管理
Shiro
- Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。
- 使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。
三個核心元件:
Subject
,SecurityManager
和Realms
.
-
Subject代表了當前使用者的安全操作,即“當前操作使用者”。
-
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。
-
Realm: Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查詢使用者及其許可權資訊。
-
ShiroBasicArchitecture
-
ShiroArchitecture
JWT
- JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案
- JSON Web令牌是一種開放的行業標準 RFC 7519方法,用於在雙方之間安全地表示宣告。
JWT 資料結構
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II
複製程式碼
JWT有三部分組成:
Header
:頭部,Payload
:負載,Signature
:簽名
SpringBoot+Shiro+JWT
pom.xml Shiro依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
複製程式碼
pom.xml JWT依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
複製程式碼
ShiroConfig.java配置
@Slf4j
@Configuration
public class ShiroConfig {
/**
* JWT過濾器名稱
*/
private static final String JWT_FILTER_NAME = "jwtFilter";
/**
* Shiro過濾器名稱
*/
private static final String SHIRO_FILTER_NAME = "shiroFilter";
@Bean
public CredentialsMatcher credentialsMatcher() {
return new JwtCredentialsMatcher();
}
/**
* JWT資料來源驗證
*
* @return
*/
@Bean
public JwtRealm jwtRealm(LoginRedisService loginRedisService) {
JwtRealm jwtRealm = new JwtRealm(loginRedisService);
jwtRealm.setCachingEnabled(false);
jwtRealm.setCredentialsMatcher(credentialsMatcher());
return jwtRealm;
}
/**
* 禁用session
*
* @return
*/
@Bean
public DefaultSessionManager sessionManager() {
DefaultSessionManager manager = new DefaultSessionManager();
manager.setSessionValidationSchedulerEnabled(false);
return manager;
}
@Bean
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean
public DefaultSubjectDAO subjectDAO() {
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
return defaultSubjectDAO;
}
/**
* 安全管理器配置
*
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(LoginRedisService loginRedisService) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jwtRealm(loginRedisService));
securityManager.setSubjectDAO(subjectDAO());
securityManager.setSessionManager(sessionManager());
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}
/**
* ShiroFilterFactoryBean配置
*
* @param securityManager
* @param loginRedisService
* @param shiroProperties
* @param jwtProperties
* @return
*/
@Bean(SHIRO_FILTER_NAME)
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
LoginService loginService,
LoginRedisService loginRedisService,
ShiroProperties shiroProperties,
JwtProperties jwtProperties) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashedMap();
filterMap.put(JWT_FILTER_NAME, new JwtFilter(loginService, loginRedisService, jwtProperties));
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterChainMap = shiroFilterChainDefinition(shiroProperties).getFilterChainMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
/**
* Shiro路徑許可權配置
*
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(ShiroProperties shiroProperties) {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// 獲取ini格式配置
String definitions = shiroProperties.getFilterChainDefinitions();
if (StringUtils.isNotBlank(definitions)) {
Map<String, String> section = IniUtil.parseIni(definitions);
log.debug("definitions:{}", JSON.toJSONString(section));
for (Map.Entry<String, String> entry : section.entrySet()) {
chainDefinition.addPathDefinition(entry.getKey(), entry.getValue());
}
}
// 獲取自定義許可權路徑配置集合
List<ShiroPermissionConfig> permissionConfigs = shiroProperties.getPermissionConfig();
log.debug("permissionConfigs:{}", JSON.toJSONString(permissionConfigs));
if (CollectionUtils.isNotEmpty(permissionConfigs)) {
for (ShiroPermissionConfig permissionConfig : permissionConfigs) {
String url = permissionConfig.getUrl();
String[] urls = permissionConfig.getUrls();
String permission = permissionConfig.getPermission();
if (StringUtils.isBlank(url) && ArrayUtils.isEmpty(urls)) {
throw new ShiroConfigException("shiro permission config 路徑配置不能為空");
}
if (StringUtils.isBlank(permission)) {
throw new ShiroConfigException("shiro permission config permission不能為空");
}
if (StringUtils.isNotBlank(url)) {
chainDefinition.addPathDefinition(url, permission);
}
if (ArrayUtils.isNotEmpty(urls)) {
for (String string : urls) {
chainDefinition.addPathDefinition(string, permission);
}
}
}
}
// 最後一個設定為JWTFilter
chainDefinition.addPathDefinition("/**", JWT_FILTER_NAME);
Map<String, String> filterChainMap = chainDefinition.getFilterChainMap();
log.debug("filterChainMap:{}", JSON.toJSONString(filterChainMap));
return chainDefinition;
}
/**
* ShiroFilter配置
*
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName(SHIRO_FILTER_NAME);
filterRegistrationBean.setFilter(proxy);
filterRegistrationBean.setAsyncSupported(true);
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return filterRegistrationBean;
}
@Bean
public Authenticator authenticator(LoginRedisService loginRedisService) {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setRealms(Arrays.asList(jwtRealm(loginRedisService)));
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
/**
* Enabling Shiro Annotations
*
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* depends-on lifecycleBeanPostProcessor
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
複製程式碼
JWT過濾器配置
@Slf4j
public class JwtFilter extends AuthenticatingFilter {
private LoginService loginService;
private LoginRedisService loginRedisService;
private JwtProperties jwtProperties;
public JwtFilter(LoginService loginService, LoginRedisService loginRedisService, JwtProperties jwtProperties) {
this.loginService = loginService;
this.loginRedisService = loginRedisService;
this.jwtProperties = jwtProperties;
}
/**
* 將JWT Token包裝成AuthenticationToken
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
String token = JwtTokenUtil.getToken();
if (StringUtils.isBlank(token)) {
throw new AuthenticationException("token不能為空");
}
if (JwtUtil.isExpired(token)) {
throw new AuthenticationException("JWT Token已過期,token:" + token);
}
// 如果開啟redis二次校驗,或者設定為單個使用者token登陸,則先在redis中判斷token是否存在
if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) {
boolean redisExpired = loginRedisService.exists(token);
if (!redisExpired) {
throw new AuthenticationException("Redis Token不存在,token:" + token);
}
}
String username = JwtUtil.getUsername(token);
String salt;
if (jwtProperties.isSaltCheck()){
salt = loginRedisService.getSalt(username);
}else{
salt = jwtProperties.getSecret();
}
return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond());
}
/**
* 訪問失敗處理
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 返回401
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 設定響應碼為401或者直接輸出訊息
String url = httpServletRequest.getRequestURI();
log.error("onAccessDenied url:{}", url);
ApiResult apiResult = ApiResult.fail(ApiCode.UNAUTHORIZED);
HttpServletResponseUtil.printJSON(httpServletResponse, apiResult);
return false;
}
/**
* 判斷是否允許訪問
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String url = WebUtils.toHttp(request).getRequestURI();
log.debug("isAccessAllowed url:{}", url);
if (this.isLoginRequest(request, response)) {
return true;
}
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { //not found any token
log.error("Token不能為空", e);
} catch (Exception e) {
log.error("訪問錯誤", e);
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 登陸成功處理
*
* @param token
* @param subject
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
String url = WebUtils.toHttp(request).getRequestURI();
log.debug("鑑權成功,token:{},url:{}", token, url);
// 重新整理token
JwtToken jwtToken = (JwtToken) token;
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
loginService.refreshToken(jwtToken, httpServletResponse);
return true;
}
/**
* 登陸失敗處理
*
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
log.error("登陸失敗,token:" + token + ",error:" + e.getMessage(), e);
return false;
}
}
複製程式碼
JWT Realm配置
@Slf4j
public class JwtRealm extends AuthorizingRealm {
private LoginRedisService loginRedisService;
public JwtRealm(LoginRedisService loginRedisService) {
this.loginRedisService = loginRedisService;
}
@Override
public boolean supports(AuthenticationToken token) {
return token != null && token instanceof JwtToken;
}
/**
* 授權認證,設定角色/許可權資訊
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.debug("doGetAuthorizationInfo principalCollection...");
// 設定角色/許可權資訊
String token = principalCollection.toString();
// 獲取username
String username = JwtUtil.getUsername(token);
// 獲取登陸使用者角色許可權資訊
LoginSysUserRedisVo loginSysUserRedisVo = loginRedisService.getLoginSysUserRedisVo(username);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 設定角色
authorizationInfo.setRoles(loginSysUserRedisVo.getRoles());
// 設定許可權
authorizationInfo.setStringPermissions(loginSysUserRedisVo.getPermissions());
return authorizationInfo;
}
/**
* 登陸認證
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.debug("doGetAuthenticationInfo authenticationToken...");
// 校驗token
JwtToken jwtToken = (JwtToken) authenticationToken;
if (jwtToken == null) {
throw new AuthenticationException("jwtToken不能為空");
}
String salt = jwtToken.getSalt();
if (StringUtils.isBlank(salt)) {
throw new AuthenticationException("salt不能為空");
}
return new SimpleAuthenticationInfo(
jwtToken,
salt,
getName()
);
}
}
複製程式碼
更多配置:github.com/geekidea/sp…
application.yml配置
############################## spring-boot-plus start ##############################
spring-boot-plus:
######################## Spring Shiro start ########################
shiro:
# shiro ini 多行字串配置
filter-chain-definitions: |
/=anon
/static/**=anon
/templates/**=anon
# 許可權配置
permission-config:
# 排除登陸登出相關
- urls: /login,/logout
permission: anon
# 排除靜態資源
- urls: /static/**,/templates/**
permission: anon
# 排除Swagger
- urls: /docs,/swagger-ui.html, /webjars/springfox-swagger-ui/**,/swagger-resources/**,/v2/api-docs
permission: anon
# 排除SpringBootAdmin
- urls: /,/favicon.ico,/actuator/**,/instances/**,/assets/**,/sba-settings.js,/applications/**
permission: anon
# 測試
- url: /sysUser/getPageList
permission: anon
######################## Spring Shiro end ##########################
############################ JWT start #############################
jwt:
token-name: token
secret: 666666
issuer: spring-boot-plus
audience: web
# 預設過期時間1小時,單位:秒
expire-second: 3600
# 是否重新整理token
refresh-token: true
# 重新整理token的時間間隔,預設10分鐘,單位:秒
refresh-token-countdown: 600
# redis校驗jwt token是否存在,可選
redis-check: true
# true: 同一個賬號只能是最後一次登陸token有效,false:同一個賬號可多次登陸
single-login: false
# 鹽值校驗,如果不加自定義鹽值,則使用secret校驗
salt-check: true
############################ JWT end ###############################
############################### spring-boot-plus end ###############################
複製程式碼
Redis儲存資訊
使用Redis快取JWTToken和鹽值:方便鑑權,token後臺過期控制等
- Redis二次校驗和鹽值校驗是可選的
127.0.0.1:6379> keys *
1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5"
2) "login:salt:admin"
3) "login:user:admin"
4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"
複製程式碼
- Redis儲存的JwtToken資訊
127.0.0.1:6379> get login:token:0f2c5d670f9f5b00201c78293304b5b5
複製程式碼
{
"@class": "io.geekidea.springbootplus.shiro.vo.JwtTokenRedisVo",
"host": "127.0.0.1",
"username": "admin",
"salt": "f80b2eed0110a7ea5a94c35cbea1fe003d9bb450803473428b74862cceb697f8",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJpc3MiOiJzcHJpbmctYm9vdC1wbHVzIiwiZXhwIjoxNTcwMzU3ODY1LCJpYXQiOjE1NzAzNTQyNjUsImp0aSI6IjE2MWQ1MDQxZmUwZjRmYTBhOThjYmQ0ZjRlNDI1ZGQ3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.0ExWSiniq7ThMXfqCOi9pCdonY8D1azeu78_vLNa2v0",
"createDate": [
"java.util.Date",
1570354265000
],
"expireSecond": 3600,
"expireDate": [
"java.util.Date",
1570357865000
]
}
複製程式碼
Reference
Shiro
JWT
spring-boot-plus