近日,大叔接公司需求,搭建前后端分离的后台管理模块接口。
简述登陆模块相关需求:
- 用户输入用户名和密码获取JWTToken令牌
- 用户使用JWTToken令牌访问后端业务接口
大叔简单说下Springboot集成Shiro的过程:
1.在pom.xml引入Shiro
<!-- Shiro使用Srping框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
2.写自己的Realm:UserRealm.java,JWTRealm.java,
/**
* 因为UserRealm只用于登陆验证故继承AuthenticatingRealm就好了
**/
public class UserRealm extends AuthenticatingRealm {
/**
* 该方法用于多Realm认证时识别需要使用哪一个Realm
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 该方法用于登陆身份验证
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//TODO 根据自己的验证需要写登陆验证
}
}
/**
* JWTRealm既要验证身份,又要做权限认证,所以继承AuthorizingRealm
**/
public class JWTRealm extends AuthorizingRealm {
/**
* 该方法用于多Realm认证时识别需要使用哪一个Realm
**/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 权限 权限验证时会执行到这里
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//TODO 根据自己的设计写权限
}
/**
* 该方法用于JWTToken验证
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//TODO 根据自己的验证需要写验证
}
}
3.写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.写ShiroConfig.java
/**
* 加载权限配置
**/
@Configuration
public class ShiroConfig {
/**
* 注册shiro的Filter,拦截请求
*/
@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();
// 设置realm,这里不设置的话会报错
// 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;
}
/**
* 用于用户名密码登录时认证的realm
*/
@Bean("userRealm")
public Realm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
/**
* 用于JWT token认证的realm
*/
@Bean("jwtRealm")
public Realm jwtRealm() {
JWTRealm jwtRealm= new JWTRealm();
return jwtRealm;
}
/**
* 初始化Authenticator 认证器 身份认证
*/
@Bean
public Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();//留意这一行哟
// 设置两个Realm,一个用于用户登录验证;一个用于jwt token的认证和访问权限获取
authenticator.setRealms(Arrays.asList(jwtRealm(), userRealm()));
// 设置多个realm认证策略,一个成功即跳过其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
/**
* 初始化authorizer 认证器 权限认证
* @return
*/
@Bean
public Authorizer authorizer() {
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();//这里的
authorizer.setRealms(Arrays.asList(jwtShiroRealm()));
return authorizer;
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
* 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,如果要完全禁用,要配合下面的noSessionCreation的Filter来实现
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
/**
* 设置过滤器,将自定义的Filter加入
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 添加过滤器
Map<String, Filter> filterMap = new HashMap<String, Filter>();
// JWT过滤器
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不做认证,noSessionCreation的作用是用户在操作session时会抛异常
chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtFilter"); // 默认进行用户鉴权
return chainDefinition;
}
// 不要加@Bean注解,不然spring会自动注册成filter,我们这里是手动注入
protected JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* 开启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的集成工作了
随便写一段测试代码
@RestController
public class TestController {
@GetMapping("/helloWorld")
@RequiresPermissions("system:test:hello")
public String helloWorld() {
return “hello world";
}
}
启动项目,请求这个接口试试看吧。
在实现了多Realm的登陆之后,发现当JWTRealm身份验证报错时,在JWTFilter获取到的异常类型都是AuthenticationException,而导致不能再JWTRealm中根据不同的异常做不同的处理。
怎么办呢?跟踪异常抛出流程发现多Realm时,异常在ModularRealmAuthenticator中会被处理掉,统一抛出AuthenticationException。所以大叔发现重写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;
}
}
重写之后替换掉ShiroConfig.java中的身份认证就好了
/**
* 初始化Authenticator 认证器 身份认证
*/
@Bean
public Authenticator authenticator() {
MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
// 设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
authenticator.setRealms(Arrays.asList(jwtShiroRealm(), dbShiroRealm()));
// 设置多个realm认证策略,一个成功即跳过其它的
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
大叔说,坑再多,不在怕,爬起来,反正还会掉坑里的