近日勋陪,大叔接公司需求,搭建前后端分離的后臺(tái)管理模塊接口史飞。
簡(jiǎn)述登陸模塊相關(guān)需求:
- 用戶輸入用戶名和密碼獲取JWTToken令牌
- 用戶使用JWTToken令牌訪問(wèn)后端業(yè)務(wù)接口
大叔簡(jiǎn)單說(shuō)下Springboot集成Shiro的過(guò)程:
1.在pom.xml引入Shiro
<!-- Shiro使用Srping框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
2.寫(xiě)自己的Realm:UserRealm.java嗓违,JWTRealm.java蛙埂,
/**
* 因?yàn)閁serRealm只用于登陸驗(yàn)證故繼承AuthenticatingRealm就好了
**/
public class UserRealm extends AuthenticatingRealm {
/**
* 該方法用于多Realm認(rèn)證時(shí)識(shí)別需要使用哪一個(gè)Realm
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 該方法用于登陸身份驗(yàn)證
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//TODO 根據(jù)自己的驗(yàn)證需要寫(xiě)登陸驗(yàn)證
}
}
/**
* JWTRealm既要驗(yàn)證身份,又要做權(quán)限認(rèn)證驰弄,所以繼承AuthorizingRealm
**/
public class JWTRealm extends AuthorizingRealm {
/**
* 該方法用于多Realm認(rèn)證時(shí)識(shí)別需要使用哪一個(gè)Realm
**/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 權(quán)限 權(quán)限驗(yàn)證時(shí)會(huì)執(zhí)行到這里
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//TODO 根據(jù)自己的設(shè)計(jì)寫(xiě)權(quán)限
}
/**
* 該方法用于JWTToken驗(yàn)證
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//TODO 根據(jù)自己的驗(yàn)證需要寫(xiě)驗(yàn)證
}
}
3.寫(xiě)JWTFilter.java
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//TODO
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//TODO
return false;
}
}
4.寫(xiě)ShiroConfig.java
/**
* 加載權(quán)限配置
**/
@Configuration
public class ShiroConfig {
/**
* 注冊(cè)shiro的Filter麻汰,攔截請(qǐng)求
*/
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean(DefaultWebSecurityManager securityManager)
throws Exception {
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>();
filterRegistration.setFilter((Filter) shiroFilter(securityManager).getObject());
filterRegistration.addInitParameter("targetFilterLifecycle", "true");
filterRegistration.setAsyncSupported(true);
filterRegistration.setEnabled(true);
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistration;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設(shè)置realm,這里不設(shè)置的話會(huì)報(bào)錯(cuò)
// One or more realms must be present to execute an authentication attempt. One
// or more realms must be present to execute an authentication attempt.
securityManager.setAuthenticator(authenticator());
securityManager.setAuthorizer(authorizer());
return securityManager;
}
/**
* 用于用戶名密碼登錄時(shí)認(rèn)證的realm
*/
@Bean("userRealm")
public Realm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
/**
* 用于JWT token認(rèn)證的realm
*/
@Bean("jwtRealm")
public Realm jwtRealm() {
JWTRealm jwtRealm= new JWTRealm();
return jwtRealm;
}
/**
* 初始化Authenticator 認(rèn)證器 身份認(rèn)證
*/
@Bean
public Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();//留意這一行喲
// 設(shè)置兩個(gè)Realm,一個(gè)用于用戶登錄驗(yàn)證戚篙;一個(gè)用于jwt token的認(rèn)證和訪問(wèn)權(quán)限獲取
authenticator.setRealms(Arrays.asList(jwtRealm(), userRealm()));
// 設(shè)置多個(gè)realm認(rèn)證策略五鲫,一個(gè)成功即跳過(guò)其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
/**
* 初始化authorizer 認(rèn)證器 權(quán)限認(rèn)證
* @return
*/
@Bean
public Authorizer authorizer() {
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();//這里的
authorizer.setRealms(Arrays.asList(jwtShiroRealm()));
return authorizer;
}
/**
* 禁用session, 不保存用戶登錄狀態(tài)。保證每次請(qǐng)求都重新認(rèn)證岔擂。
* 需要注意的是位喂,如果用戶代碼里調(diào)用Subject.getSession()還是可以用session,如果要完全禁用乱灵,要配合下面的noSessionCreation的Filter來(lái)實(shí)現(xiàn)
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* 設(shè)置過(guò)濾器塑崖,將自定義的Filter加入
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 添加過(guò)濾器
Map<String, Filter> filterMap = new HashMap<String, Filter>();
// JWT過(guò)濾器
filterMap.put("jwtFilter", jwtFilter());// JwTfilter
factoryBean.setFilters(filterMap);
// 攔截器
factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());
return factoryBean;
}
@Bean
protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login", "noSessionCreation,anon"); // login不做認(rèn)證,noSessionCreation的作用是用戶在操作session時(shí)會(huì)拋異常
chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtFilter"); // 默認(rèn)進(jìn)行用戶鑒權(quán)
return chainDefinition;
}
// 不要加@Bean注解阔蛉,不然spring會(huì)自動(dòng)注冊(cè)成filter弃舒,我們這里是手動(dòng)注入
protected JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* 開(kāi)啟Shiro注解通知器
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
好了,到這里我們可以算作是完成了Shiro的集成工作了
隨便寫(xiě)一段測(cè)試代碼
@RestController
public class TestController {
@GetMapping("/helloWorld")
@RequiresPermissions("system:test:hello")
public String helloWorld() {
return “hello world";
}
}
啟動(dòng)項(xiàng)目,請(qǐng)求這個(gè)接口試試看吧聋呢。
在實(shí)現(xiàn)了多Realm的登陸之后苗踪,發(fā)現(xiàn)當(dāng)JWTRealm身份驗(yàn)證報(bào)錯(cuò)時(shí),在JWTFilter獲取到的異常類(lèi)型都是AuthenticationException削锰,而導(dǎo)致不能再JWTRealm中根據(jù)不同的異常做不同的處理通铲。
怎么辦呢?跟蹤異常拋出流程發(fā)現(xiàn)多Realm時(shí),異常在ModularRealmAuthenticator中會(huì)被處理掉器贩,統(tǒng)一拋出AuthenticationException颅夺。所以大叔發(fā)現(xiàn)重寫(xiě)ModularRealmAuthenticator中的doMultiRealmAuthentication方法就好了
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(MultiRealmAuthenticator.class);
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
throws AuthenticationException {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException e) {
authenticationException = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm
+ "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if (authenticationException != null) {
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
重寫(xiě)之后替換掉ShiroConfig.java中的身份認(rèn)證就好了
/**
* 初始化Authenticator 認(rèn)證器 身份認(rèn)證
*/
@Bean
public Authenticator authenticator() {
MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
// 設(shè)置兩個(gè)Realm,一個(gè)用于用戶登錄驗(yàn)證和訪問(wèn)權(quán)限獲扔忌浴吧黄;一個(gè)用于jwt token的認(rèn)證
authenticator.setRealms(Arrays.asList(jwtShiroRealm(), dbShiroRealm()));
// 設(shè)置多個(gè)realm認(rèn)證策略,一個(gè)成功即跳過(guò)其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
大叔說(shuō)唆姐,坑再多拗慨,不在怕,爬起來(lái)奉芦,反正還會(huì)掉坑里的