SpringBoot整合shiro实现用户的认证授权

  • * 项目环境搭建
  • * 配置ShiroConfig,用于shiro的基本配置和注入自定义规则
  • * 实现自定义的realm,继承AuthorizingRealm
  • * 编写测试controller和页面
  1. 基本环境准备
  2. 导入依赖坐标
  3. maven管理、shiro1.4.0 和spring-shiro1.4.0依赖
  4. 导入数据源,配置thymeleaf,redis,等等

 

  1. shiro配置
  2. 配置shiroConfig
  3. 编写自定义的realm
  4. 实现具体的doGetAuthorizationInfo(授权)方法和doGetAuthenticationInfo(认证)

具体实现:

shiroConfig配置

@Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm){
        DefaultWebSecurityManager ds = new DefaultWebSecurityManager();
        ds.setRealm(myRealm);
        return ds;
    }
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager){
        ShiroFilterFactoryBean sf = new ShiroFilterFactoryBean();
        //设置安全管理器
        sf.setSecurityManager(defaultSecurityManager);
        sf.setLoginUrl("/login");
        sf.setUnauthorizedUrl("/non");
        sf.setSuccessUrl("/index");
        /**
         * 自定义过滤器
         * anon:
         * authc:
         * user: 只有实现了remberme的操作才能访问
         * perms: 必须得到资源权限才能访问
         * role: 必须得到角色权限的时候才能访问
         */
        Map<String,String> china = new LinkedHashMap<>();
        china.put("/index","authc");
        china.put("/update","authc");
        china.put("/non","authc");
        china.put("/toLogin","anon");
        china.put("/add","perms[user:add]");
        china.put("/**","anon");
        sf.setFilterChainDefinitionMap(china);
        return sf;
    }
    @Bean(name = "myRealm")
    public MyRealm getRealm(){
        return new MyRealm();
    }

自定义编写realm 继承AuthorizingRealm 实现doGetAuthorizationInfo 和doGetAuthenticationInfo方法

/**
     * 自定义授权逻辑
     * Authorization
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //给资源授权
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //加上授权字符串(当前用户已经授权)
        simpleAuthorizationInfo.addStringPermission("user:add");
        return simpleAuthorizationInfo;
    }
    /**
     * 自定义认证的逻辑
     *  判断用户名和密码
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		//直接强转
        UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken;
        System.out.println("处理用户登录逻辑");
        char[] password = auth.getPassword();
        String username = auth.getUsername();
        if(!"user".equals(username)){
            //返回null 会抛出 UnknownAccountException
            return null;
        }
		//用户传入的密码 数据库中加载出来的密码
        return new SimpleAuthenticationInfo(password,"123","");
    }

 首先配置shiro的配置类使用@Configuration注解类上,这里面我们需要基本的三个配置类
    @Bean
    ShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager)

    @Bean(name = "securityManager")
    DefaultWebSecurityManager (@Qualifier("myRealm") MyRealm myRealm)

    @Bean(name = "myRealm")
    MyRealm()

    第一个是可以自定义认证授权规则,配置权限拦截规则指定跳转页面
    第二个是将shiro的安全管理器注入,然后返回
    第三种是自定义实现自己认证授权逻辑

自定义realm
继承自AuthorizingRealm,实现其中的两个方法
`protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)`

1. 此方法是授权逻辑的实现,指定用户可以拥有那些权限,将其set到addStringPermission集合中即可
2. 其中授权类是其的一个子类
3. SimpleAuthorizationInfo simpleAuthorizationInfo

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)

  此方式认证逻辑,用于判断用户是否登录成功,或者异常     
    用户名判断错误返回 null 
    密码使用new SimpleAuthenticationInfo(用户输入的密码,数据库的密码,"");

举个例子:

/**
     * 自定义授权逻辑
     * Authorization
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //给资源授权
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //加上授权字符串(当前用户已经授权)
        simpleAuthorizationInfo.addStringPermission("user:add");
        return simpleAuthorizationInfo;
    }

    /**
     * 自定义认证的逻辑
     *  判断用户名和密码
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken;
        System.out.println("处理用户登录逻辑");
        char[] password = auth.getPassword();
        String username = auth.getUsername();
        if(!"user".equals(username)){
		//返回null 会抛出 UnknownAccountException
            return null;
        }
        return new SimpleAuthenticationInfo(password,"123","");
    }
    /**
     * 自定密码判断
     *
     * @param pass
     * @return
     */
    private boolean isPassWord(char[] pass){
        String password = "123";
        if(pass.length != password.length()){
            return false;
        }
        char[] chars = password.toCharArray();
        for(int i = 0; i < pass.length; i++){
            if(chars[i] != pass[i]){
                return false;
            }
        }
        return true;
    }

// 当然授权也可以在controller层中通过@RequiresPermissions("user:update")注解在当前用户操作的地址上授权
<p style="color:red">
    注意: 这里面需要在shiroconfig中配置以下内容:
</p>
    
    /**
     * 解决@RequiresPermissions("XX:XXX:...")注解无效
     *
     * @return
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(defaultSecurityManager);
        return advisor;
    }

  并且在controller里面配置的权限检验,用户验证失败,会跳转到/error页面上,需要自己自定义页面

注意:
这里面有个大坑,开始使用如下代码发现并未解决问题

    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.put("org.apache.shiro.authz.UnauthorizedException","/non");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }

最后通过springMVC的异常处理类指定跳转页面,才解决此问题。

    @ControllerAdvice
    public class MyExceptionHandler {
    
        @ExceptionHandler(UnauthenticatedException.class)
        public String nauthenticatedException(Model model){
            model.addAttribute("errorMsg","当前无用户!");
            return "nonauth";
        }
        @ExceptionHandler(UnauthorizedException.class)
        public String nauthorizedException(Model model){
            model.addAttribute("errorMsg","当前用户没有权限");
            return "nonauth";
        }
    }

不错
现在已经解决了两个问题:

1. 在用户未登录的时候,直接访问带有@RequiresPermissions权限的注解时会报错,而不是跳转到指定页面,或者返回相应的信息
2. 未使用在remal配置的权限过滤器的时候,而是在@Controller上直接带有@RequiresPermissions("user:add") 会报500错误,而不是可控操作


在doGetAuthenticationInfo方法中,获取用户登录时候的信息操作,验证用户,将用户存入session,以后通过subject对象强转。
    

     SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(salt),
                //realm name
                getName());

    user 是开始通过用户名查询到的用户信息
    salt是加盐处理,硬编码加盐处理

#shiro密码匹配#
    
这里先来个简单的直接加盐 然后使用MD5加密方式
密码匹配则时先将用户输入的密码加盐再字符串比较

    实现自定义的Realm 继承自AuthorizingRealm
    
    doGetAuthenticationInfo方法中使用

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
        用户对象
                user,
        数据库用户密码
                user.getPassword(),
        加盐处理
                ByteSource.Util.bytes(salt),
                //realm name
                getName());    
    
    将用户对象保存至
        String string = MD5Util.encryptString(password+salt);
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,string);

使用shiro的HashedCredentialsMatcher自定义密码加密    
先模拟用户和密码数据 使用单元测试:

    @Test
    public void contextLoads() {
        String algorithmName = "md5";
        String username = "admin";
        String password = "123";
        String salt1 = username;
        String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
        int hashIterations = 3;
        SimpleHash hash = new SimpleHash(algorithmName, password,
                salt1 + salt2, hashIterations);
        String encodedPassword = hash.toHex();
        System.out.println(encodedPassword);
        System.out.println(salt2);
    } 

将生成的数据保存到数据库中,验证的时候将密码和盐拿出来,这时候,用户的注册名和密码在注册的时候保存到数据库中

在Shiro的配置类中,注入

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     *  所以我们需要修改下doGetAuthenticationInfo中的代码;
     * )
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new MyHashedCredent();
        //使用指定的散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        //几次散列?
        hashedCredentialsMatcher.setHashIterations(3);
        //设置16进制编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

  


在将其自定义的密码加载类注入到自定义的realm中

    @Bean(name = "myRealm")
    public MyRealm getRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher){
        MyRealm myRealm = new MyRealm();
        // 设置自定义加密
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return myRealm;
    }

下面实现自定义密码:
    

    
    /**
     * @author zhangyi
     * @date 2018/12/12 20:43
     */
    public class MyHashedCredent extends HashedCredentialsMatcher {
    public MyHashedCredent(){}
    /**
     * 以后放到redis中保存
     */
    private Cache<String, AtomicInteger> passwordRetryCache;

    public MyHashedCredent(CacheManager cacheManager) {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        //限制每个用户的请求登录次数(密码错误的时候)
        String username = (String) token.getPrincipal();
        if(!Objects.isNull(passwordRetryCache)) {
            // retry count + 1
            AtomicInteger retryCount = passwordRetryCache.get(username);
            if (retryCount == null) {
                retryCount = new AtomicInteger(0);
                passwordRetryCache.put(username, retryCount);
            }
            if (retryCount.incrementAndGet() > 5) {
                // if retry count > 5 throw
                throw new ExcessiveAttemptsException();
            }
        }
        boolean matches = super.doCredentialsMatch(token, info);
        if (matches) {
            // clear retry count
    //     passwordRetryCache.remove(username);
        }
        return matches;
    }
    }

  


按照开涛神的方法,计算其密码重试的次数,将其保存到EhCache中,这里我保存到redis,因为好用吧。
开始使用硬编码加盐,网上说可能会有安全问题,现在采用
    
    用户名+密码+随机数 --> 在N次散列 加密,应该好一点点

我这里是继承了HashedCredentialsMatcher这中加密方式,还可以继承SimpleCredentialsMatcher,或者继承PasswordMatcher,或者实现CredentialsMatcher接口来加密,不过本质上差不多

posted @ 2019-02-19 13:52  孤燕南飞  阅读(1697)  评论(0编辑  收藏  举报