SpirngBoot + Shiro 实现多种登录方式的身份认证
一、Shiro中的类
核心类
ShiroFilterFactoryBean:Shiro框架中的Web过滤器
SecurityManager:Shiro的核心,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务,构建ShiroFilterFactoryBean时需要一个SecurityManager实例。
Subject:“当前操作用户”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
Realm: 充当了Shiro与应用安全数据间的“桥梁”或者“连接器,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。SecurityManager需设置1个或者多个Realm实例。
认证功能相关
Token:用户令牌。shiro对用户执行认证的过程,即是将从Realm查找的系统用户信息与用户提交的Token校验的过程。
UsernamePasswordToken:主要带用户名及密码信息的token。
FormAuthenticationFilte:用户验证过滤器,用户认证时,由FormAuthenticationFilte根据request表单信息生成UsernamePasswordToken。
二、在SpringBoot中集成Shiro
pom.xml中加入shiro组件
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
三、实现认证功能
1、配置ShiroFilterFactoryBean;
2、配置SecurityManager;
@Configuration
/***
* 系统权限管理配置,基于Shiro框架实现
* @author PPY
*/
public class CodeBoxShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//过滤链,从上向下顺序执行,一般将/**放在最后边
LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置可以匿名访问的url,顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/image/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
//配置退出url,其中的过滤器退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//配置必须认证通过才可以访问的url
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//将自定义的FormAuthenticationFilter注入shiroFilter中。
//主要是用于构造自定义令牌,实现多种登录方式
//当不设置自定义Filter时,默认为“authc”构建FormAuthenticationFilter实例
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new CodeBoxFormAuthenticationFilter());
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(codeBoxAuthorizingRealm());
return securityManager;
}
@Bean
public CodeBoxAuthorizingRealm codeBoxAuthorizingRealm(){
CodeBoxAuthorizingRealm codeBoxAuthorizingRealm = new CodeBoxAuthorizingRealm();
return codeBoxShiroRealm;
}
}
3、配置一个自定义AuthorizingRealm类,重写 doGetAuthorizationInfo()及 doGetAuthenticationInfo()方法;
public class CodeBoxAuthorizingRealm extends AuthorizingRealm {
@Resource
private UserService userService;
/**
* 授权信息
* **/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User userInfo = (User)principals.getPrimaryPrincipal();
// Todo 根据userInfo添加用户角色及权限信息到AuthorizationInfo中
//authorizationInfo.addRole();
//authorizationInfo.addStringPermission();
return authorizationInfo;
}
/**
* 身份信息。这里只实现用户名+密码验证方式
* **/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {
String username = (String)cbToken.getPrincipal();
//通过username从数据库中查找 User对象.
User userInfo = userService.findByUsername(username);
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户信息
userInfo.getCredentials(),//验证信息
getName() //realm name
);
return authenticationInfo;
}
}
4、实现LoginController
@Controller
public class LoginController {
@RequestMapping("/login")
public String Login(HttpServletRequest request, Map<String, Object> map) throws Exception{
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute(CodeBoxFormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
String logintype=(String)request.getAttribute(CodeBoxLoginToken.LOGIN_TYPE);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
if(CodeBoxLoginToken.LOGIN_TYPE_FP.equals(logintype)) {
msg = "系统未注册该指纹账户";
}else {
msg = "账号不存在";
}
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
if(CodeBoxLoginToken.LOGIN_TYPE_FP.equals(logintype)) {
msg = "指纹信息后台验证不通过";
}else {
msg = "密码不正确";
}
} else if ("kaptchaValidateFailed".equals(exception)) {
msg = "验证码错误";
} else {
msg = exception;
}
}
map.put("msg", msg);
map.put(CodeBoxLoginToken.LOGIN_TYPE, logintype);
// 此方法不处理登录成功,由shiro进行处理
return "/login";
}
}
注意事项:
1)ShiroFilterFactoryBean中url拦截器注意顺序,需使用LinkedHashMap,“anon”在最前,“authc”在最后。
2)将添加"/css","/image", 等静态资源url到”anon“过滤器中,否则会导致/login页面无法加载静态资源,问题直观现象是浏览器出现"CSS 因 Mime 类型不匹配而被忽略"等问题,页面显示异常。
3)codeBoxAuthorizingRealm需加@Bean注解,否则内部的@Resource 无法成功注入,问题想象表现为登录验证时出现出现userService空指针异常。
四、实现多种认证方式
1、实现自定义Token类,
继承UsernamePasswordToken并新增登录方式、其他登录验证信息等属性。
public class CodeBoxLoginToken extends UsernamePasswordToken {
private static final long serialVersionUID = 7134536615448037793L;
public static final String LOGIN_TYPE_FP="TYPE1";
public static final String LOGIN_TYPE="logintype";
public static final String FP_INFO="fpInfo";
/**
*登陆类型
*/
private String loginType;
private String fpInfo;
public CodeBoxLoginToken(String username, String password, boolean rememberMe, String host,
String logintype,String fpinfo) {
super(username, password, rememberMe, host);
this.loginType = logintype;
this.fpInfo= fpinfo;
}
//省略get,set
}
2、实现自定义FormAuthenticationFilter 类
重写createToken方法,返回自定义Token类对象。
public class CodeBoxFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request);
String password = getPassword(request);
String host = getHost(request);
boolean rememberme = isRememberMe(request);
String logintype = WebUtils.getCleanParam(request, CodeBoxLoginToken.LOGIN_TYPE);
String fpinfo = WebUtils.getCleanParam(request, CodeBoxLoginToken.FP_INFO);
return new CodeBoxLoginToken(username, password, rememberme,host,logintype, fpindex,fpdevid);
}
}
3、修改前文中的自定义AuthorizingRealm类
修改 doGetAuthenticationInfo()的方法,适配不同登录类型
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {
if(!(token instanceof CodeBoxLoginToken)) {
return null;
}
CodeBoxLoginToken cbToken=(CodeBoxLoginToken)token;
//登录方式
String logintype =(String)cbToken.getLoginType();
//获取用户的输入的账号,
String username = (String)cbToken.getPrincipal();
String fpinfo = (String)cbToken.getFpInfo();
//通过username从数据库中查找 User对象.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User userInfo=null;
String credentials="";
if(CodeBoxLoginToken.LOGIN_TYPE_FP.equals(logintype)) {
if(username==null||fpindex==null) {
return null;
}
userInfo = userService.findByfpInfo(fpinfo);
if(userInfo!=null) {
//TODO 根据实际情况获取验证信息
//credentials = 。。。;
}
}else {
userInfo = userService.findByUsername(username);
if(userInfo!=null) {
credentials = userInfo.getUpassword();
}
}
if(userInfo == null){
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户信息
credentials,//验证信息
getName() //realm name
);
return authenticationInfo;
}
4、在ShiroFilterFactoryBean中添加自定义FormAuthenticationFilter 作为“authc”过滤器。
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//省略部分代码
。。。
//将自定义的FormAuthenticationFilter注入shiroFilter中。
//主要是用于构造自定义令牌,实现多种登录方式
//当不设置自定义Filter时,默认为“authc”构建FormAuthenticationFilter实例
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new CodeBoxFormAuthenticationFilter());
return shiroFilterFactoryBean;
}
注意事项:
不可将自定义FormAuthenticationFilter 配置为Bean,否则将作为与shirFilter同一级别的过滤器,对所有url(“/**”)进行权限验证,导致/login页面无法加载静态资源,导致页面显示异常。
五、解决重复登录不正常的问题
问题现象
成功登录系统后未注销时,在login页面重新异常请求登录,无法正常跳转到SuccessUrl。
问题原因
查看系统源码,核心登录流程如下图所示,登陆认证时,FormAuthenticationFilter首先通过onPreHandle检查当前访问是否被允许(isAccessAllowed),若允许则直接跳过当前的认证过滤器。否则,调用OnAccessDenied检查当前访问是否应该被拒绝,并在OnAccessDenied检查时执行身份认证流程,并且认证通过跳转到SuccessUrl。
executeLogin.png
进一步查看isAccessAllowed方法在AuthenticatingFilter、AuthenticationFilter 两个父类中的实现(如下),可知,若当前用户已经认证过了(subject.isAuthenticated()),则认为当前访问是被允许的,从而onPreHandle()的处理结果是当前访问无需进一步身份认证。结果为拦截器通行。
即导致在登录成功且未登出的情况下,若用户自行退回应用界面,再次点击登录按钮进行异常登录请求/login时,无法再次实现“成功登录则跳转到SuccessUrl,失败仍然返回‘/login’页面"的需求。
public abstract class AuthenticatingFilter extends AuthenticationFilter {
/**
* Determines whether the current subject should be allowed to make the current request.
* <p/>
* The default implementation returns <code>true</code> if the user is authenticated. Will also return
* <code>true</code> if the {@link #isLoginRequest} returns false and the "permissive" flag is set.
*
* @return <code>true</code> if request should be allowed access
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
}
public abstract class AuthenticationFilter extends AccessControlFilter {
/**
* Determines whether the current subject is authenticated.
* <p/>
* The default implementation {@link #getSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) acquires}
* the currently executing Subject and then returns
* {@link org.apache.shiro.subject.Subject#isAuthenticated() subject.isAuthenticated()};
*
* @return true if the subject is authenticated; false if the subject is unauthenticated
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
}
解决方法
在CodeBoxFormAuthenticationFilter 中重写isAccessAllowed方法,若当前请求是登录url,并且当用户已经登录了,则返回false,以便继续验证新的登录信息
public class CodeBoxFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//是登录url,并且当用户已经登录了,则返回false,以便继续验证新的登录信息
if(isLoginRequest(request, response) && isLoginSubmission(request, response)){
return false;
}
return super.isAccessAllowed(request, response, mappedValue);
}
//省略其他
}