SpirngBoot + Shiro 实现多种登录方式的身份认证

2019-03-10  本文已影响0人  大P还是小p

一、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 &quot;permissive&quot; 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);  
    }
    //省略其他
}
上一篇下一篇

猜你喜欢

热点阅读