Subject 工厂(非必须)
无状态的 Web 服务(RESTful),意味着我们不会使用 Shiro 的 Session 功能,更用不上 SessionDAO 。因此,严谨的做法可以在 Security Manager 的配置中将这两个功能关闭掉。
当然,你不关闭也可以,不使用它们即可。
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
通过调用 context.setSessionCreationEnabled(false)
表示不创建会话;如果之后调用 subject.getSession()
将抛出 DisabledSessionException
异常。
在之前的『页面跳转式』的项目中,Shiro 将 Subject 的信息存入了 Session 中,并且每个 Subject 是有『状态』的概念的<small>(已经登录、未登录)</small>。
JwtAuthenticationFilter
类似于 FormAuthenticationFilter,但是根据当前请求上下文信息每次请求时都要登录的认证过滤器。
AccessControlFilter 有两个方法:isAccessAllowed 方法和 onAccessDenied 方法:
-
如果
isAccessAllowed()
返回 true ,Shiro 将放过请求,允许访问 URL<small>(这时 Shiro 不考虑 onAccessDenied() 方法)</small>,接下来流程就会走到 Controller ; -
如果
isAccessAllowed()
返回 false,那么 Shiro 再来考虑onAccessDenied()
方法。onAccessDenied()
方法返回 true,则表示请求继续,返回 fase,则表示认证不通过,需要返回给用户登陆页面<small>(或表示错误信息的 JSON 信息)</small>。
@Slf4j
public class JwtAuthenticationFilter extends AccessControlFilter {
/**
* 该方法决定 JwtFilter 是否放行请求。
* 该方法返回 true,那么当前请求将走向 Controller 。
* 该方法返回 false,那么当前请求有可能走向下一个 Filter,也有可能就此打回。这取决于下面的 onAccessDenied 方法的返回值。
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
Subject subject = getSubject(request, response);
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String jwt = httpServletRequest.getHeader("x-jwt-token");
log.info("jwt: {}", jwt);
if (!StringUtils.hasText(jwt)) {
log.info("进入 onAccessDenied 方法");
return false;
}
JwtToken token = new JwtToken(jwt);
try {
subject.login(token);
log.info("jwt 合法,进入Controller。");
return true;
} catch (RuntimeException e) {
e.printStackTrace();
log.info("进入 onAccessDenied 方法");
return false;
}
}
/**
* 该方法决定当前请求在不符合当前 Filter 的同行标准的前提下,接下来是走向下一个 Filter ,还是就此打回。
* 该方法返回 true,那么当前请求将走向下一个 Filter 。
* 该方法返回 false,那么当前请求将不再触发下一个 Filter 的执行,就此打回。
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 当前请求到此为止了:控制跳转显示登录页面,或显示 JSON 串
onAuthenticatedFailure(request, response);
return false;
}
/**
* 私有方法,onAccessDenied 返回 false 时调用,控制重定向登录页面,或返回 JSON 字符串。
*/
private void onAuthenticatedFailure(ServletRequest request, ServletResponse response) throws IOException {
// 跳转页面
saveRequestAndRedirectToLogin(request, response);
/*
// 返回 JSON 串
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("unlogin");
*/
}
和客户端约定好,要求客户端发送的请求,在请求头中传递 jwt-token-string ,然后生成 JwtToken 对象,再触发 Security Manager 进行 subject 的认证。
JwtToken
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken (String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
用户身份和凭证都是 token 。
JwtRealm
用于认证的 Realm 。
public class JwtRealm extends AuthorizingRealm {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) token;
String jwt = jwtToken.getJwt();
log.info("待校验的 jwt 为 {}", jwt);
SimpleAuthenticationInfo info = null;
try {
jwtUtil.isVerify(jwt);
log.info("realm 认为 jwt 合法");
info = new SimpleAuthenticationInfo(jwt, jwt, getName());
return info;
} catch (RuntimeException e) {
log.info("relm 认为 jwt 非法");
e.printStackTrace();
}
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 逻辑上,doGetAuthorizationInfo 方法与 JwtAuthenticationFilter 的工作无关。
}
}
基于 starter 整合 JWT
@Configuration
public class ShiroConfig {
@Bean
public JwtRealm realm() {
JwtRealm realm = new JwtRealm();
return realm;
}
/**
* 由于使用自定义 Realm,所以要显式配置 SecurityManager 。
*/
@Bean
public DefaultWebSecurityManager securityManager(JwtRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
/**
* 非必须。以下配置与 Shiro 整合 JWT 的核心功能无关。
*/
// 关闭 ShiroDAO 功能。
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
// 禁止 Subject 的 getSession 方法
// manager.setSubjectFactory(new JwtDefaultSubjectFactory());
return manager;
}
/**
* 由于需要自定义 filter chain,所以相关配置要配置在 ShiroFilterFactoryBean 中,而不是『页面跳转』式项目那样的 ShiroFilterChainDefinition 里。
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login-page"); // 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403-page");
/*
* 定义Filter链
*/
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("anon", new AnonymousFilter());
filters.put("jwt", new JwtFilter()); // 这里只能 new ,不能 @Autowired
shiroFilterFactoryBean.setFilters(filters);
/*
* 拦截规则
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/login-page", "anon");
filterChainDefinitionMap.put("/login", "anon");
// 认证靠 jwt-filter,鉴权靠注解。
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 但是 ShiroFilterChainDefinition 的配置仍然需要。
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
return new DefaultShiroFilterChainDefinition();
}
// @Bean
// @DependsOn("lifecycleBeanPostProcessor")
// public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
// DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制指定注解的底层实现使用 cglib 方案
// defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
// return defaultAdvisorAutoProxyCreator;
// }
}
网友评论