首先定义一个ShiroConfig配置类,在此类里面创建sessionManager和securityManager对象,并把他们放入IOC中,然后对自定义的过滤器进行定义,主要就是把定义的filter放入到过滤器链中
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限 使用jwtFilter进行过滤
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
然后创建shiroFilterFactoryBean,FactoryBean需要看他的getObject方法
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
然后进入createInstance方法
protected AbstractShiroFilter createInstance() throws Exception {
logdebug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
//省略部分代码
//创建FilterChain管理器,会将Shiro默认的Filter加入进来,同时将配置文件中的Filter加进来
FilterChainManager manager = createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
此factoryBean的作用,只需要把自定义好的filter放入shiroFilterFactoryBean中就可以在程序运行中执行自定义的filter
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
这里出现的jwtFilter就是自定义的filter继承AuthenticatingFilter,他主要的作用就是对每一次请求进行拦截,做一些校验
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
JwtUtils jwtUtils;
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
/*
* 进行拦截,如果没有token则不走Realm,直接访问controller,若无权限判断则访问成功,若有权限判断则访问失败
* */
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return true;
}else {
//校验jwt
Claims claim = jwtUtils.getClaimByToken(jwt);
if (ObjectUtils.isEmpty(claim) || jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new ExpiredCredentialsException("token失效,请重新登录");
}
//执行登录
return executeLogin(servletRequest,servletResponse);
}
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result result = Result.fail(throwable.getMessage());
String json = JSONUtil.toJsonStr(result);
try {
httpServletResponse.getWriter().print(json);
} catch (IOException ex) {
}
return false;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
createToken方法是从请求中获取token信息,然后对String类型的token进行转换成JwtToken
onAccessDenied方法是真正的拦截方法,判断请求是否带有token,没有token即为未登录状态,有token的为登录状态,若没有token直接放行,因为游客也是有一定的权限的比如登录权限…若有token则需要判断这个token师傅合法是否失效等,最后调用executeLogin方法,即为提交给登录,这里就到了shiro著名的Realm中,源码是
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = this.createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
} else {
try {
Subject subject = this.getSubject(request, response);
subject.login(token);
return this.onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException var5) {
return this.onLoginFailure(token, var5, request, response);
}
}
}
在源码中发现调用成功会调用onLoginSucccess方法,而失败的onLoginFailure方法需要在Filter类中重写,目的是统一封装返回格式为Result
最后的preHandle是解决跨区问题的
然后再看createToken方法中把String对象封装成JwtToken的类,只需要实现AuthenticationToken
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt) {
this.token = jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
最后,自定义的Realm,总结来说就是继承AuthorizingRealm,然后重写认证doGetAuthorizationInfo方法,从token中获取user信息,查询数据库比对信息,若为正常用户,则把用户的信息封装到SimpleAuthorizationInfo对象中
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserService userService;
/*
* 告知shoir,token支持JwtToken
* */
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/*
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/*
* 认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//需要调用supports
JwtToken jwtToken = (JwtToken) authenticationToken;
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
User user = userService.getById(Long.valueOf(userId));
if (ObjectUtils.isEmpty(user)) {
throw new UnknownAccountException("用户不存在");
}
if (user.getStatus() == -1) {
throw new LockedAccountException("账户已被锁定");
}
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(user, profile);
//公开信息
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
这里值得说明的是在Reaml的认证方法中不需要对username进行比对,因为这都是login接口需要做的