无状态应用中Shiro登录验证的处理

在传统JSP web应用中,如果用shiro处理授权的话,通常通过FormAuthenticationFilter来直接截取用户提交的表单,并从中取出usernamepasswordrememberMe三个参数来进行登录验证(截取的参数名可以在配置文件中手动修改)。
但是这并不适用于RESTful风格的应用,因为前端的用户验证信息是通过一个http的请求头请求发送过来的,没办法通过FormAuthenticationFilter进行处理,所以我们需要实现自己的filter。

无状态Filter的实现

此处为了正常处理登录验证和授权的功能,我们需要继承类HttpMethodPermissionFilter并实现两个方法

1
2
3
4
5
6
//访问被拒绝时调用此方法
protected boolean onAccessDenied(ServletRequest request,ServletResponse response)
throws IOException {}
//访问被接受时调用此方法
public boolean isAccessAllowed(ServletRequest request,ServletResponse response,Object mappedValue)
throws IOException,UnknownAccountException,IncorrectCredentialsException,UnknownError {}

isAccessAllowed方法实现

此方法是整个授权认证的入口,负责从请求中获取用户信息并提交给SecurityManager进行授权认证,并在认证成功后将结果添加到request域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public boolean isAccessAllowed(ServletRequest request,ServletResponse response, Object mappedValue)
throws IOException,UnknownAccountException,IncorrectCredentialsException,UnknownError {
HttpServletRequest req = (HttpServletRequest) request;
String name = req.getHeader("name");//从请求头获取用户名
String pass = req.getHeader("pass");//从请求头获取凭证
StatelessToken token = new StatelessToken();
// 如果是带验证的,则进行验证,否则没有验证,只能进行一般的请求
if (name != null && pass != null) {
token.setPrincipal(new UserPrincipal(name, UserPrincipal.PrincipType.USER));
token.setPassword(pass);
try {
getSubject(request, response).login(token);
// 如果认证成功,则增加request的属性,用于@CurrentUser注解使用
Employee employee = token.getEmployee();
request.setAttribute(Contants.CURRENT_USER, employee);
} catch (UnknownAccountException e) {
logger.info("用户不存在! "+e.getClass().getSimpleName());
request.setAttribute("shiroLoginFailure",UnknownAccountException.class.getName());
} catch (IncorrectCredentialsException e){
logger.info("用户名/密码错误! "+e.getClass().getSimpleName());
request.setAttribute("shiroLoginFailure",IncorrectCredentialsException.class.getName());
}
catch (Exception e) {
logger.info("其他认证失败! "+e.getClass().getSimpleName());
request.setAttribute("shiroLoginFailure",UnknownError.class.getName());
e.printStackTrace();
}
}
return super.isAccessAllowed(request, response, mappedValue);
}

需要注意的是为了在验证没通过时给前端返回对应错误信息,而控制器又没有直接调用认证的方法,所以需要此处捕获到异常并放入request域来方便controller进行处理。在使用FormAuthenticationFilter的案例中前者帮我们处理了这步骤。

处理登录请求的控制器实现

这里的ErrorResponseEntity是自行根据需要封装的错误实体类,其中最后是调用了org.springframework.httpResponseEntity.status().body();来对Response的信息进行了封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("login")
public ResponseEntity login(HttpServletRequest request, @CurrentUser User user){
//如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
//根据shiro返回的异常类路径判断,抛出指定异常信息
if(exceptionClassName!=null){
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
return ErrorResponseEntity.buildToResponseEntity(401, "用户不存在",HttpStatus.UNAUTHORIZED);
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
return ErrorResponseEntity.buildToResponseEntity(401, "用户名/密码错误",HttpStatus.UNAUTHORIZED);
}else {
return ErrorResponseEntity.buildToResponseEntity(401, "其他错误",HttpStatus.UNAUTHORIZED);
}
}
//如果登录成功,则调用服务返回用户可访问的菜单项.
return ResponseEntity.ok(resourceService.getMenuByUserId(employee.getUserId()));
}

本文实现方法修改参考自ichenkaihua

具体用户身份和认证令牌的封装请参考以上项目