SpringBoot进阶(十)整合Shiro上篇

1,603 阅读6分钟

GitHub:github.com/baiyuliang/…

一般的,SpringBoot常用的安全模块有Spring下的Security和Apache下的Shiro,Security功能强大但复杂,Shiro则相对小而简单,通常的,Shiro能满足我们实际开发中的绝大部分需求,所以使用Shiro的开发人员也越来越多了!

安全模块的作用:

  • 身份认证 (登录验证/加密)
  • 授权(授予权限、角色)
  • Session管理
  • 加密
  • 记住我
  • ...

所以,安全模块,是一个web网站必不可少的东西了!关于Shiro的详细使用方法,大家可以查看相关文档,本篇属于实战型案例,整合Shiro+加密加盐+角色权限+Session管理+Redis+Shiro标签使用,所以关于更多细节上的东西,还需要大家自行查看文档加以理解!

由于这一篇涉及到了角色和权限,那么在上一篇已经提到过的这两个表要必须创建了,再贴一下:

在这里插入图片描述

Role表:

在这里插入图片描述

Permission表:

在这里插入图片描述

用户表:

在这里插入图片描述

注意用户表,前面我们用到的密码,都是明文方式,这肯定是不可取的,这里我改回了加密方式并且是加盐(salt)加密,后面会讲到,数据大家可以自行添加,对应的Role和Permission的javabean不要忘了添加!

在这里插入图片描述

首先分析登录认证流程:

  1. 用户属于账号、密码登录;
  2. Controller接收到参数后进入Shiro认证流程;
  3. Shiro验证用户名和密码,通过后保存登录信息并进入授权流程,不通过返回错误信息;
  4. 授权时,需要根据登录信息,获取该用户的角色,以及该角色对应的权限,并通过Shiro绑定;

根据上面的分析,我们需要先创建Role和Permission表对应的Dao和Service:

RoleRepository

package com.byl.springbootdemo.dao;

import com.byl.springbootdemo.bean.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {

}

RoleService:

package com.byl.springbootdemo.service;

import com.byl.springbootdemo.bean.Role;

public interface RoleService {

    Role getRoleById(Integer id);
}

RoleServiceImpl:

package com.byl.springboottest.service;

import com.byl.springboottest.bean.Role;
import com.byl.springboottest.dao.RoleRepository;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class RoleServiceImpl implements RoleService {

    @Resource
    RoleRepository roleRepository;

    @Override
    public Role getRoleById(Integer id) {
        return roleRepository.findById(id).get();
    }


}


PermissionRepository:

package com.byl.springbootdemo.dao;

import com.byl.springbootdemo.bean.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface PermissionRepository extends JpaRepository<Permission, Integer> {

    Permission findByRoleId(Integer role_id);

}

PermissionService:

package com.byl.springbootdemo.service;

import com.byl.springbootdemo.bean.Permission;

public interface PermissionService {

    Permission getPermissionByRoleId(Integer role_id);
}

PermissionServiceImpl:

package com.byl.springboottest.service;

import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.dao.PermissionRepository;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class PermissionServiceImpl implements PermissionService {

    @Resource
    PermissionRepository permissionRepository;

    @Override
    public Permission getPermissionByRoleId(Integer role_id) {
        return permissionRepository.findByRoleId(role_id);
    }

}

pom.xml引入:

        <!--权限认证框架shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.6.0</version>
        </dependency>
        <!-- shiro整合redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.3.1</version>
        </dependency>
        <!-- thymeleaf整合shiro标签 -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

application添加属性:

# Session超时时间(默认30分钟)
shiro.session.expireTime=30
shiro.jessionid=byl.sessionId

注意,我们使用了Shiro,那么上一篇所使用的的拦截器就不用再使用了,因为拦截器所实现的功能,Shiro已经提供了,大家可以自行注释掉!

创建PermissionRealm:

package com.byl.springboottest.shiro;

import com.byl.springboottest.bean.Permission;
import com.byl.springboottest.bean.Role;
import com.byl.springboottest.bean.User;
import com.byl.springboottest.service.PermissionService;
import com.byl.springboottest.service.RoleService;
import com.byl.springboottest.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

public class PermissionRealm extends AuthorizingRealm {

    @Resource
    UserService userService;
    @Resource
    RoleService roleService;
    @Resource
    PermissionService permissionService;

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("进入授权>>");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        Role role = roleService.getRoleById(user.getRoleId());
        System.out.println("角色>>"+role.getName());
        simpleAuthorizationInfo.addRole(role.getName());//角色:superadmin,admin,user
        Permission permission = permissionService.getPermissionByRoleId(role.getId());
        System.out.println("权限>>"+permission.getName());
        simpleAuthorizationInfo.addStringPermission(permission.getName());//添加权限
        return simpleAuthorizationInfo;
    }

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        String username = (String) authenticationToken.getPrincipal();
        //获取用户信息
        User user = userService.getUserByName(username);
        if (user == null) {
            return null;
        } else {
            ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());//参数要与加密时方式一致(用户名+盐值)
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName());
            clearCachedAuthorizationInfo();
            return simpleAuthenticationInfo;
        }
    }

    /**
     * 清理缓存权限
     */
    public void clearCachedAuthorizationInfo() {
        this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
    }

}

重写AuthorizingRealm 的两个方法:doGetAuthorizationInfo 授权和doGetAuthorizationInfo认证,由于使用了加盐方式,我们需要一个Util类,来处理盐值的生成与密码的加密逻辑:

SaltUtil:

package com.byl.springbootdemo.utils;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class SaltUtil {

    public static String HASHALGORITHMNAME = "md5";//加密方式
    public static int HASHITERATIONS = 1024;//加密次数

    public static String randomSalt() {
        // 一个Byte占两个字节,此处生成的3字节,字符串长度为6
        SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator();
        String hex = secureRandom.nextBytes(3).toHex();
        return hex;
    }

    public static String encryptPassword(String username, String password) {
        String salt= randomSalt();
        String newPassword = new SimpleHash(HASHALGORITHMNAME, password,
                ByteSource.Util.bytes(username +salt),
                HASHITERATIONS
        ).toHex();
        System.out.println("salt>>"+salt);
        System.out.println("newPassword>>"+newPassword);
        return newPassword;
    }
}

什么是加盐? 就是撒盐哥往牛排上撒点盐?非也!加盐其实是对普通的md5等加密方式做了进一步处理,目的就是防止暴力破解,基本上无解!我们知道,普通的加密方式,就是对明文密码进行1-2次的md5加密,虽然不可逆,但为了尽可能的安全(防止被密码库暴力破解),此时就可以在原来明文密码的基础上,加一些“杂质”混合进去,而且这些杂质还是动态的,那么就算大罗金仙来了,也破解不了了!如上面的代码,密码生成方式为:

  1. 先生成随机盐值,每个用户的盐值都不相同;
  2. 加密时,将用户名+盐值,混合用户自己输入的密码加密,得到最终密码;
  3. 记录生成的盐值和最终密码,存入数据库;

在登录验证时,就可以通过数据库中保存的盐值和密码,与用户输入的账号和密码进行验证了:

ByteSource salt = ByteSource.Util.bytes(user.getUsername() + user.getSalt());//参数要与加密时方式一致(用户名+盐值)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName())

创建ShiroConfig:

package com.byl.springboottest.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.byl.springboottest.shiro.PermissionRealm;
import com.byl.springboottest.shiro.RolesAuthorizationFilter;
import com.byl.springboottest.utils.SaltUtil;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.timeout}")
    private int redisTimeout;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Value("${spring.redis.password}")
    private String redisPassword;

    @Value("${shiro.session.expireTime}")
    private int expireTime;

    @Value("${shiro.jessionid}")
    private String jessionId;


    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //对所有用户认证
        map.put("/**", "authc");
        //允许访问
        map.put("/css/**", "anon");
        map.put("/images/**", "anon");
        map.put("/js/**", "anon");
        map.put("/lib/**", "anon");
        map.put("/login.html", "anon");
        map.put("/user/login", "anon");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error/403.html");
        //页面角色权限控制
        map.put("/level1/**", "anyRoleFilter[user,admin,superadmin]");
        map.put("/level2/**", "anyRoleFilter[admin,superadmin]");
        map.put("/level3/**", "anyRoleFilter[superadmin]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("anyRoleFilter", new RolesAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 自定义密码校验器
     *
     * @return
     */
    @Bean
    public CredentialsMatcher credentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName(SaltUtil.HASHALGORITHMNAME);
        credentialsMatcher.setHashIterations(SaltUtil.HASHITERATIONS);
        return credentialsMatcher;
    }

    //将自己的验证方式加入容器
    @Bean
    public PermissionRealm permissionRealm(CredentialsMatcher credentialsMatcher) {
        PermissionRealm customRealm = new PermissionRealm();
        customRealm.setCredentialsMatcher(credentialsMatcher);
        return customRealm;
    }

    /**
     * 配置SecurityManager
     *
     * @return
     */
    @Bean
    public SecurityManager securityManager(CredentialsMatcher credentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(permissionRealm(credentialsMatcher));  // 设置realm
        securityManager.setSessionManager(sessionManager());    // 设置sessionManager
        securityManager.setCacheManager(myRedisCacheManager()); // 设置cacheManager
        return securityManager;
    }


    /**
     * redisCacheManager 缓存 redis实现
     * shiro-redis
     * We need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal.
     * For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.
     * Default value is "id", that means your principal object has a method called "getId()"
     *
     * @return
     */
    @Bean
    public RedisCacheManager myRedisCacheManager() {
        RedisCacheManager cacheManager = new RedisCacheManager();
        cacheManager.setRedisManager(redisManager());
        cacheManager.setPrincipalIdFieldName("username");//主键名称(默认id)
        return cacheManager;
    }

    /**
     * 配置shiro redisManager
     * shiro-redis
     *
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost + ":" + redisPort);
        redisManager.setTimeout(redisTimeout);
        redisManager.setPassword(redisPassword);
        return redisManager;
    }

    /**
     * SessionManager
     * shiro-redis
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(expireTime * 60 * 1000);
        sessionManager.setSessionIdUrlRewritingEnabled(false);//禁用url重写,否则浏览器中会在url后面自动加上:xx/login;JSESSIONID=xxx
        sessionManager.setSessionIdCookie(getSessionIdCookie());
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * 给shiro的sessionId默认的JSSESSIONID名字改掉
     *
     * @return
     */
    @Bean
    public SimpleCookie getSessionIdCookie() {
        SimpleCookie simpleCookie = new SimpleCookie(jessionId);
        return simpleCookie;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * shiro-redis
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        sessionDAO.setSessionIdGenerator(sessionIdGenerator());
        return sessionDAO;
    }

    /**
     * Session ID 生成器
     *
     * @return
     */
    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /***
     * 使授权注解起作用
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    //配置ShiroDialect:用于thymeleaf和shiro标签配合使用
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

}


RolesAuthorizationFilter:

package com.byl.springbootdemo.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.List;

/**
 * 角色授权过滤器
 */
public class RolesAuthorizationFilter extends AuthorizationFilter {

    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            return false;
        }

        List<String> roles = CollectionUtils.asList(rolesArray);
        boolean[] hasRoles = subject.hasRoles(roles);
        for (boolean hasRole : hasRoles) {
            if (hasRole) {
                return true;
            }
        }
        return false;
    }
}

我将资源文件做了一些更改:

在这里插入图片描述

大家可以在这里下载源码,以供参考:download.csdn.net/download/ba…

UserController里的登录逻辑,我们需要重新来写了:

    @PostMapping("/login")
    public ResponseData login(@RequestParam Map<String, String> params) {
        logger.error(params.toString());
        if (params.get("username") == null || params.get("password") == null) return new ResponseData(-1, "账号或账号不能为空");
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(params.get("username"), params.get("password"));
//        usernamePasswordToken.setRememberMe(true);//记住密码
        try {
            subject.login(usernamePasswordToken);
            subject.hasRole("admin");
        } catch (UnknownAccountException e) {
            return new ResponseData(-1, "用户不存在");
        } catch (AuthenticationException e) {
            return new ResponseData(-1, "账号或密码错误");
        } catch (AuthorizationException e) {
            return new ResponseData(-1, "没有权限");
        }
        return new ResponseData(1, "登录成功");
    }

关于参数常用的接收方式有以下几种:

  • JavaBean:比如,login(@RequestBody User user){},可以直接取对象里面的字段,但记住一定要有@RequestBody注解(提交的参数要与字段对应);
  • Map:如上面的代码,要有@RequestParam注解(提交的参数要与key对应);
  • 具体参数:如login(String username,String password){}(参数注解可以省略,提交的参数要与方法参数对应);

此时,我们就可以来做登录验证了,我们可以通过网页注册的形式,将加密过的密码存入数据库,但我并没有实现,这个实现就交给你们了!我是使用test方法,直接生成salt和密码,并存入了数据库,大家测试阶段也可以这么做,能省不少时间!

    @Test
    void testPwdSalt(){
        SaltUtil.encryptPassword("admin","admin");
    }

执行结果:

salt>>d1af77
newPassword>>c4b33995b676a712c5b48a3c4fa38e85

记录这两个值,存入账号为admin的数据库,可以自己生成!

浏览器进入登录页http://localhost:8080/byl/login,开始验证:

在这里插入图片描述

在这里插入图片描述

登录成功,查看控制台:

在这里插入图片描述 授权成功!

注意Controller中的:

subject.login(usernamePasswordToken);
subject.hasRole("admin");

这两句代码,第一个就是登录验证,第二个就是授权!此时打开redis管理器: 在这里插入图片描述

证明,和redis已经整合成功!