在登录入口类中先创建用户名/密码身份验证Token。
UsernamePasswordToken token = new UsernamePasswordToken(phone, password);
然后调用subject.login(token);此时SecurityManager将会委托Authenticator进行身份验证,Authenticator会将token传入Realm从Realm获取身份验证信息,如果没有返回或抛出异常表示身份验证失败。用户登录成功可以将用户身份信息放入shiroSession中,在自定义的sessionExpiredFilter类中通过判断请求的session中是否存在用户信息,处理过期的登录请求。(浏览器每次请求会将sessionId通过request传给服务端,通过sessionId获取当前用户的session信息)
Subject subject = SecurityUtils.getSubject();
User user = userService.getUserByPhone(phone);
//创建用户名、密码身份验证Token
UsernamePasswordToken token = new UsernamePasswordToken(phone, password);
String loginToken = new Md5Hash(new Date().toString() + ShiroSession.getId()).toHex();
try {
subject.login(token);
ShiroSession.set("user_info", JSON.toJSONString(user));
//如果登录成功且设置了7天自动登录
if (remember == true) {
userService.updateToken(user.getPhone(), loginToken);
//设置客户端cookie保存7天
CookieUtil.addCookie(response, "loginToken", loginToken, 7);
}
return ResponseUtils.buildOKResponse(JSON.toJSONString(new User(user.getId(), user.getNickName())));
UserRealm继承AuthorizingRealm类,实现两个方法doGetAuthenticationInfo和doGetAuthorizationInfo
在doGetAuthenticationInfo中从通过
String phone = (String) token.getPrincipal();获得角色信息
String password = new String((char[]) token.getCredentials());获得登录密码
然后与从数据库中查询出的用户密码进行匹配,进行判断逻辑处理,无误后将用户的登录信息存入SimpleAuthenticationInfo并返回:
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
phone,
password,
ByteSource.Util.bytes(phone),//设置username、pwd都没问题
getName() //realm name(唯一)
);
return info;
在doGetAuthorizationInfo类中主要实现对已认证的用户分配资源权限:
String phone = (String) principalCollection.getPrimaryPrincipal();获得认证用户,然后通过自定义的getResourcesByUserId方法获取用户资源列表,放入HashSet中,最后调用SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(new HashSet<>(permissions));将集合内容填充到认证中
ResourceCheckFilter继承AccessControlFilter实现两个方法isAccessAllowed和onAccessDenied,负责访问路径的认证判断:
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o)
throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
String url = getPathWithinApplication(servletRequest);
return subject.isPermitted(url);
}
isAccessAllowed方法通过获取request的请求路径,调用isPermitted方法,判断该资源是否可用,如果返回true,走到下一个过滤器。如果没有下一个过滤器的话,表示具有了访问某个资源的权限,如果返回 false,则会调用 onAccessDenied 方法,去实现相应的当过滤不通过的时候执行的操作。
onAccessDenied方法对没有访问权限的页面做处理:
@Override
@ResponseBody
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse)
throws Exception {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (WebUtil.isAjax(request)) {
//返回403 FORBIDDEN状态码
response.setStatus(403);
}
//如果是普通请求,进行页面重定向
else {
response.sendRedirect(request.getContextPath()+"/403");
}
return false;
}
实现PermissionResolver接口,它负责解析权限字符串到Permission实例,RolePermissionResolver用于根据角色解析相应的权限集合。
WildcardPermission是shiro默认的权限字符串的表示方式,此处根据我们自己的url格式自定义UrlPermission类进行权限匹配。
UrlPermission 是一个实现了Permission接口的类,它的implies方法的实现决定了权限是否匹配。
public class UrlPermissionResolver implements PermissionResolver {
@Override
public Permission resolvePermission(String s) {
if (s.startsWith("/")) {
return new UrlPermission(s);
} else {
return new WildcardPermission(s);
}
}
}
class UrlPermission implements Permission {
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public UrlPermission() {
}
public UrlPermission(String url) {
this.url = url;
}
@Override
public boolean implies(Permission permission) {
if (!(permission instanceof UrlPermission)) {
return false;
}
UrlPermission urlPermission = (UrlPermission) permission;
PatternMatcher patternMatcher = new AntPathMatcher();
return patternMatcher.matches(this.getUrl(), urlPermission.getUrl());
}
}
/assets/** = anon
/admin/**= sessionExpiredFilter,user,resourceFilter
可以看到上面spring-shiro的shiroFilter拦截器中还定义了一个sessionExpiredFilter,在该过滤器中我们实现自己的自动登录逻辑和登录过期请求的处理:
首先在用户登录认证代码中如果用户登录成功,且选择了自动登录选项,我们保存用户的登录状态信息到session中,然后生成一个loginToken认证码,更新到user表的字段中:
String loginToken = new Md5Hash(new Date().toString() + ShiroSession.getId()).toHex();
try {
subject.login(token);
ShiroSession.set("user_info", JSON.toJSONString(user));
//如果登录成功且设置了7天自动登录
if (remember == true) {
userService.updateToken(user.getPhone(), loginToken);
//设置客户端cookie保存7天
CookieUtil.addCookie(response, "loginToken", loginToken, 7);
}
return ResponseUtils.buildOKResponse(JSON.toJSONString(new User(user.getId(), user.getNickName())));
}catch(){
}
在自定义的SessionExpiredFilter过滤器中写http请求的前置拦截逻辑代码,先判断session中用户的登录状态,若不存在则用户未登录,然后判断request请求中是否存在loginToken(未选择自动登录或者已过7天token失效),若存在,通过该token值从数据库中查询与之匹配的用户,若存在,则执行登录代码,若不存在,则执行逻辑跳转处理:
public class SessionExpiredFilter extends PathMatchingFilter {
@Autowired
UserService userService;
@Override
protected boolean onPreHandle(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (ShiroSession.get("user_info") != null) {
return true;
} else {
//判断请求端带过来cookie是否存在
String loginToken = CookieUtil.findCookieByName(request, "loginToken");
if (StringUtils.isNotBlank(loginToken)) {
//到数据库查询有没有该Cookie
User user = userService.findUserByToken(loginToken);
if (user != null) {
try {
//重新登录
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getPhone(), user.getPassword());
subject.login(token);
ShiroSession.set("user_info", JSON.toJSONString(user));
return true;
} catch (AuthenticationException e) {
e.printStackTrace();
return false;
}
} else {
//没有该Cookie与之对应的用户(Cookie不匹配)
CookieUtil.clearCookie(request, response, "loginToken");
return false;
}
} else {
//没有登录,也没有cookie凭证
if (WebUtil.isAjax(request)) {
response.setStatus(401);
} else {
response.sendRedirect(request.getContextPath() + "/401");
}
return false;
}
}
}
}
在此过程中尝试过使用UserInterceptor,在拦截器中处理登录过期请求,但是shiroFilter过滤器的处理顺序在Spring的拦截器之前执行,处理请求无法到达interceptor。
配置中的注入的shiroFilter在web.xml文件中通过DelegatingFilterProxy对servlet filter的代理,来实现自定义的filter使配置的shiro拦截器生效。
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
/*