超详细springboot+apache shiro+redis

以此文章为自己学习总结用,希望各位大哥多多指正。

简介:

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。下面我们来看看shiro架构图

1.Subject:用户体,这里所指的用户不单单是人可以是程序,总的来说就是任何与此应用就交互的东西,与Subject进行交互的东西都会被委托给Security Manager,Security Manager(Shiro 核心三大模块之一)是实际的执行者。

2.Security Manager:shiro的核心组件,用于协调各个组件之间的使用。

3.Authenticator:用于进行登录认证的组件

4.Authorizer:用于进行授权认证的组件,在登录完成之后进行授权,决定这Subject拥有什么样的角色和权限。

5.SessionManager:会话管理者组件, 创建和管理用户Session

6.CacheManager:缓存管理组件,一般用来存储用户的Session和权限信息,从而到达在一定的时间不必每次请求数据源来判断Subject角色信息。

7.Cryptography:数据加密组件

8.Realm:数据源,充当与程序和数据之间的中间角色,主要负责用户登录认证和授权认证。

三大核心组件:Subject, SecurityManager 和 Realms

使用:

pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wzh</groupId>
    <artifactId>springboot_shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_shiro</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <!--把下列注释起来,因为在新版当中某些用户使用这个会报错。注释起来就好了.-->
          <!--  <scope>runtime</scope>-->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

一、使用之前做一些准备工作,创建好测试使用的几张表:

user用户表:                                                                      

role角色表:

user_role用户角色表:

permission权限表:

role_permission权限角色表:

二、创建实体类,这块比较简单就直接贴代码了,注意的是创建的实体类需要实现可序列号接口,不然在后期与Redis整合的时候会报错

//权限类
public class Permission implements Serializable {

    private int id;
    private String name;
    private String url;

    public int getId() {return id;}

    public void setId(int id) {this.id = id;}

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}

    public String getUrl() {return url;}

    public void setUrl(String url) {this.url = url;}
}
//角色类
public class Role implements Serializable {

    private int id;
    private String name;
    private String description;
    private List<Permission> permissionList;
    public List<Permission> getPermissionList() {return permissionList;}

    public void setPermissionList(List<Permission> permissionList) {
        this.permissionList = permissionList;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
//角色权限类
public class RolePermission {
    private int id;
    private int roleId;
    private int permissionId;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getRoleId() {
        return roleId;
    }

    public void setRoleId(int roleId) {
        this.roleId = roleId;
    }

    public int getPermissionId() {
        return permissionId;
    }

    public void setPermissionId(int permissionId) {
        this.permissionId = permissionId;
    }
//用户类
public class User implements Serializable {
    private int id;
    private String username;
    private String password;
    private Date createTime;
    private List<Role> roleList;
    private List<Permission> permissionList;


    public List<Permission> getPermissionList() {
        return permissionList;
    }

    public void setPermissionList(List<Permission> permissionList) {
        this.permissionList = permissionList;
    }

    public List<Role> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<Role> roleList) {
        this.roleList = roleList;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

}
//用户角色类
public class UserRole {
    private  int id;
    private int userId;
    private  int roleId;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getRoleId() {
        return roleId;
    }

    public void setRoleId(int roleId) {
        this.roleId = roleId;
    }

}

二、Dao层,使用的是Mybatis。

//userMapper
@Mapper
public interface UserMapper {
    
    @Select("select * from user where username=#{username}")
    User findByUsername(@Param("username") String username);

    @Select("select * from user where id=#{userId}")
    User findById(@Param("userId") int id);

    @Select("select * from user where name=#{username} and password = #{pwd}")
    User findByUsernameAndPwd(@Param("username") String username, @Param("pwd") String pwd);
}
//PermissionMapper 
@Mapper
public interface PermissionMapper {

    @Select("SELECT t4.id id,t4.name name,t4.url url FROM role_permsission t3 LEFT JOIN permission t4 ON t4.id = t3.permission_id WHERE t3.role_id = #{roleId}")
    List<Permission> findPermissionListByRoleId(@Param("roleId") int roleId);
}
//RoleMapper 
@Mapper
public interface RoleMapper {
        @Select("SELECT t.role_id id, t1.`name` NAME,t1.description description FROM user_role t LEFT JOIN role t1  on t.role_id = t1.id WHERE t.user_id = #{userId}")
    @Results(value = {
            @Result(id=true,property = "id",column = "id"),
            @Result(property = "name",column = "name"),
            @Result(property = "description",column = "description"),
            @Result(property = "permissionList",column = "id",
                    many = @Many(select = "com.wzh.springboot_shiro.dao.PermissionMapper.findPermissionListByRoleId",fetchType = FetchType.DEFAULT)),
    }
            )
    List<Role> findRoleByUserId(@Param("userId") int userId);
}

三、service层级实现层

public interface UserService {
    /**
     * 获取全部用户信息包括角色权限
     * @param username
     * @return
     */
    User findAllUserInfoByUserName(String username);

    /**
     * 获取用户基本信息
     * @param userId
     * @return
     */
    User findSimpleUserInfoById(int userId);

    /**
     * 根据用户名查找用户信息
     * @param username
     * @return
     */
    User findSimpleUserInfoByUsername(String username);
}
@Service
public class UserServiceimpl implements UserService {
    //部分用户在自动注入的这块可能会报红,是因为idea没识别的原因,并不会影响使用。
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PermissionMapper permissionMapper;
    @Override
    public User findAllUserInfoByUserName(String username) {
        User user = userMapper.findByUsername(username);
        List<Role> roleList = roleMapper.findRoleByUserId(user.getId());
        user.setRoleList(roleList);
        return user;
    }

    @Override
    public User findSimpleUserInfoById(int userId) {
        return userMapper.findById(userId);
    }

    @Override
    public User findSimpleUserInfoByUsername(String username) {
        return userMapper.findByUsername(username);
    }
}

四、定义Web层返回信息格式

public class ResultApi {

    public static Map<String, Object > ResultNoAndDesc(ApiResponseEnum apiResponseEnum, boolean flag) {
        Map<String,Object> result = new HashMap<>();
        result.put("no",apiResponseEnum.getNo());
        result.put("msg",apiResponseEnum.getMsg());
        if(flag){
            result.put("data",apiResponseEnum.getDesc());
        }
        return result;
    }
    public static Map<String, Object> ResultAll(ApiResponseEnum apiResponseEnum,Object object) {
        Map<String,Object > result = new HashMap<>();
        result.put("no",apiResponseEnum.getNo()+"");
        result.put("msg",apiResponseEnum.getMsg());
        result.put("data",object);
        return result;
    }
}
/**
 * web 层返回信息枚举
 */
public enum ApiResponseEnum {

    NEED_LOGIN(-2,"success","温馨提示:请使用对应的账号登录"),
    REFUSE_PERMIT(-3,"fail","温馨提示:拒绝访问,权限不足!"),
    SUCCESS_STATUS(1,"success"),
    SUCCESS_STATUS_NULL(1,"fail"),
    FAIL_STAUTS(0,"fail"),
    LOGIN_SUCCESS(1,"success","登录成功!"),
    LOGIN_FAIL(1,"success","登录失败!"),
    LOGOUT_SUCCESS(1,"success","退出登录!"),
    ;
    private int no ;

    private String msg;

    private String desc;

    private ApiResponseEnum(int no, String msg, String desc) {
       this.no = no;
       this.msg = msg;
       this.desc = desc;

    }
    private ApiResponseEnum(int no, String msg) {
        this.no = no;
        this.msg = msg;

    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

springboot配置文件

server.port=8089
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/my_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#开启控制台打印sql
#mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true

准备工作到此告一段了,下面开始使用Shiro

五、shiro使用

5.1、我们在config包里自定义一个类 CustomRealm继承AuthorizingRealm实现两个父类的方法,doGetAuthorizationInfo、doGetAuthenticationInfo

doGetAuthenticationInfo:进行登录验证的,我们可以在这里面进行自己的逻辑验证规则

doGetAuthorizationInfo:进行登录验证之后,在访问某些需要特定权限的页面时,会调用此方法进行授权认证,判断是否有次权限或角色。

我首先简单写下登录验证逻辑。

public class CustomRealm extends AuthorizingRealm {

    //注入Service层
    @Autowired
    private UserService service;
    /**
     * 进行授权认证操作.
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 进行登录验证操作逻辑
     * @param authenticationToken 用户输入的token信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("打桩:正在进行登录验证....");
        //从token中获取用户输入的信息
        String username = (String) authenticationToken.getPrincipal();
        //从数据库中查询用户名为username的用户信息
        User user = service.findAllUserInfoByUserName(username);
        //获取密码
        String pwd = user.getPassword();
        //简单判断
        if(pwd == null || "".equals(pwd)){
            //说明不存在此用户信息。
            return null;
        }
        //存在,返回一个认证信息
        return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
    }
}

5.2、创建一个ShiroConfig用来自定义我配置的权限规则,并自定义一个返回类型为ShiroFilterFactoryBean工厂Bean,并在里面实现自己的过滤规则

这里有个注意的地方:方法的参数SecurityManager导包的时候记得是shiro下的包,并且导入的时候一直会报错,针对于这一点我们暂时可以手动注入SecurityManager这个Bean对象来解决,具体原因我也不知道。

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        System.out.println("打桩:ShiroFilterFactoryBean");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        return shiroFilterFactoryBean;

    }  

    /**
     * SecurityManager 核心组件,也正是我们前面所说的执行者,他用来绑定我们后期大多数自定义的        
       逻辑
     * 列如Session之类的都是通过它绑定到我们的Shiro中。
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        return securityManager;
    }

}

5.3、注入我们自定义的CustomRealm并在SecurityManager绑定我们自己定义的CustomRealm。

    @Bean
    public  CustomRealm customRealm (){
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    }
 @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //绑定自定义的Realm
        securityManager.setRealm(customRealm());
        return securityManager;
    }

5.4、接下来我们就将SecurityManager添加到我们的shiroFilterFactoryBean这个方法中,并在shiroFilterFactoryBean中分配一下接口的访问权限。

@Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        System.out.println("打桩:ShiroFilterFactoryBean");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //将我们刚刚定义的SecurityManager设置到shiroFilter中
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        /**
         * 接下来就是分配一下我们接口的访问权限
         */
        //需要登录访问的接口
        shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
        //登录成功,跳转url,如果前后端是分离开发的则没有这个调用
        shiroFilterFactoryBean.setSuccessUrl("/");
        //登录了,没有权限则调用此接口,验证登录->权限验证
        shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");
        //权限过滤器
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        //退出过滤器
        filterChainDefinitionMap.put("/logout","logout");
        //匿名可以访问,也就是可以访问的公共资源
        filterChainDefinitionMap.put("/pub/**","anon");
        //登录才可以访问的
        filterChainDefinitionMap.put("/authc/**","authc");
        //管理员权限访问的
        filterChainDefinitionMap.put("/admin/**","roles[admin]");
        //有编辑权限才可以访问的
        filterChainDefinitionMap.put("/video/update","perms[video_update]");

        filterChainDefinitionMap.put("/**","authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }

在这里我解释一下方法的作用:

setLoginUrl():当你访问某个接口的时候,这个接口需要是在登录的状态下才能访问而你没有登录,那么它就会自动调用方法内的接口。setUnauthorizedUrl():当你访问某个接口的时候,这个接口需要在特定的权限下才能访问而你没有此权限就会调用方法内的接口。

上面两个方法内我们可以存放一下提示信息(提示登录或者提示权限不足)。

配置权限过滤器:有两个需要注意到的地方。

1.Map应该使用LinkedHashMap,因为拦截器拦截应该是有序的,直接使用HashMap(无序的)的话会产生拦截效果时而有效时而无效。

2.记得配置一个全局变量放在最后,避免一些不必要的错误。

关于filterChainDefinitions的参数稍微解释一下,这些规则是自上而下有序执行的。

anon        org.apache.shiro.web.filter.authc.AnonymousFilter
authc       org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic  org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms       org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port        org.apache.shiro.web.filter.authz.PortFilter
rest        org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles       org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl         org.apache.shiro.web.filter.authz.SslFilter
user        org.apache.shiro.web.filter.authc.UserFilter


annon:表示url地址是可以任何人都能访问的(匿名访问、游客访问)
authc:表示url地址是需要登录认证后才能够访问
perms:之过滤规则,一般都是扩展应用不适应原生的。
user:用户登录可以访问的
roles:与perms类似,授权过滤器
port:端口认证
ssl:表示安全的url请求,协议为https 
rest:根据请求的方法POST、GET、DELETE

anon,authcBasic,auchc,user是认证过滤器,  
perms,roles,ssl,rest,port是授权过滤器  

5.5、我们写一个Controller用来存在公共的接口路径

@RestController
@RequestMapping("pub")
public class PubController {
    //需要登录提示
    @RequestMapping("need_login")
    public Map<String, Object> needLogin(){
        return ResultApi.ResultNoAndDesc(ApiResponseEnum.NEED_LOGIN,true);
    }
    //权限不足提示
    @RequestMapping("not_permit")
    public Map<String,Object> notPermit(){
        return ResultApi.ResultNoAndDesc(ApiResponseEnum.REFUSE_PERMIT,true);
    }
    //首页
    @RequestMapping("index")
    public Map<String,Object> index(){
        List<String> videoList = new ArrayList<>();
        videoList.add("Mysql零基础入门到实战,数据教程");
        videoList.add("Redis高并发高可用集群百万级秒杀实战");
        videoList.add("Zookeeper+Dubbo视频教程 微服务教程分布式教程");
        videoList.add("2019年新版本RocketMQ4.x教程消息队列教程");
        videoList.add("微服务SpringCloud+Docker入门到高级实战");

        return ResultApi.ResultAll(ApiResponseEnum.SUCCESS_STATUS,videoList);
    }
    //登录操作
    @PostMapping("login")
    public Map<String,Object> login(@RequestBody UserQuery userQuery,
                                    HttpServletRequest request, HttpServletResponse response){
        /**
         * UserQuery:类,登录时候使用的手动创建一个实体类,只有用户名和密码两个成员变量。
         */
        try{
            //得到用户体
            Subject subject = SecurityUtils.getSubject();
            //将用户信息放入Token中
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userQuery.getName(),userQuery.getPassword());
            //进行登录校验
            subject.login(usernamePasswordToken);
            Map<String,String> loginmap = new HashMap<>();
            //校验成功返回一个SessionId
            loginmap.put("msg","登录成功");
            loginmap.put("session_id",subject.getSession().getId()+"");
            return ResultApi.ResultAll(ApiResponseEnum.LOGIN_SUCCESS,loginmap);
        }catch (Exception e){
            e.printStackTrace();
            return ResultApi.ResultNoAndDesc(ApiResponseEnum.LOGIN_FAIL,true);
        }
    }
}

我们来简单测试一下目前的功能。

首先访问index主页(任何人可见的)

访问一下登录操作(失败)

登录操作(成功):成功的话会返回一个SessionId,我们需要拿到这个id去进行后续权限访问。

后台输出(注意一下打桩的地方):

@PostMapping("login")
    public Map<String,Object> login(@RequestBody UserQuery userQuery,
                                    HttpServletRequest request, HttpServletResponse response){
        /**
         * UserQuery:类,登录时候使用的手动创建一个实体类,只有用户名和密码两个成员变量。
         */
        try{
            //得到用户体
            Subject subject = SecurityUtils.getSubject();
            //将用户信息放入Token中
            System.out.println("1");
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userQuery.getName(),userQuery.getPassword());
            //进行登录校验
            System.out.println("2");
            subject.login(usernamePasswordToken);
            System.out.println("3");
            Map<String,String> loginmap = new HashMap<>();
            //校验成功返回一个SessionId
            loginmap.put("msg","登录成功");
            loginmap.put("session_id",subject.getSession().getId()+"");
            return ResultApi.ResultAll(ApiResponseEnum.LOGIN_SUCCESS,loginmap);
        }catch (Exception e){
            e.printStackTrace();
            return ResultApi.ResultNoAndDesc(ApiResponseEnum.LOGIN_FAIL,true);
        }
    }

可以看到,在进行登录操作的时候当进行到Subject.login的时候会去执行我们之前自定义的CustomRealm中的逻辑进行校验。

5.6、写出几个刚才在ShiroConfig符合定义路径规则的几个权限路径的Controller返回的消息可以自己简单定义下,下面是我简单定义的一些。

@RestController
@RequestMapping("admin")
public class AdminController {
    @RequestMapping("/video/order")
    public Map<String,Object> findMyPlayRecord(){
        Map<String,String> recordMap = new HashMap<>();
        recordMap.put("SpringBoot入门到高级实战","第8章第1集");
        recordMap.put("Cloud微服务入门到高级实战","第1章");
        recordMap.put("分布式缓存Redis","第10章第3集");
        recordMap.put("Zookeeper+dubbo","第10章第3集");
        return ResultApi.ResultAll(ApiResponseEnum.SUCCESS_STATUS,recordMap);
    }
}
<------------------------------------------------------------------------------------->
@RestController
@RequestMapping("authc")
public class OrderController {
    @RequestMapping("/video/play_record")
    public Map<String,Object> findMyPlayRecord(){
        Map<String,String> recordMap = new HashMap<>();
        recordMap.put("SpringBoot入门到高级实战","第8章第1集");
        recordMap.put("Cloud微服务入门到高级实战","第1章");
        recordMap.put("分布式缓存Redis","第10章第3集");
        return ResultApi.ResultAll(ApiResponseEnum.SUCCESS_STATUS,recordMap);
    }
}
<------------------------------------------------------------------------------------->
@RestController
@RequestMapping("video")
public class VideoController {
    @RequestMapping("/update")
    public Map<String,Object> findMyPlayRecord(){
        return ResultApi.ResultAll(ApiResponseEnum.SUCCESS_STATUS,"更新成功!");
    }
}

接下来我们需要继续在刚才自定义的CustomRealm中完成授权认证的那部分模块。

@Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("打桩:正在进行授权认证");
        //得当用户信息
        User newuser = (User) principalCollection.getPrimaryPrincipal();
        //得到用户角色和权限信息
        User user = service.findAllUserInfoByUserName(newuser.getUsername());
        //创建两个集合来存储用户的角色和权限
        List<String> stringRoleList = new ArrayList<>();
        List<String> stringPermissionList = new ArrayList<>();

        //从user中得到角色信息
        List<Role> roleList = user.getRoleList();
        //遍历集合将角色信息添加到stringRoleList中
        for (Role role : roleList) {
            stringRoleList.add(role.getName());
            //同时也遍历Role将权限信息添加到stringPermissionList中
            List<Permission> permissionList = role.getPermissionList();
            for (Permission permission : permissionList) {
                if (permission != null) {
                    stringPermissionList.add(permission.getName());
                }
            }
        }
        //返回一个授权认证信息
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(stringRoleList);
        simpleAuthorizationInfo.addStringPermissions(stringPermissionList);
        return simpleAuthorizationInfo;
    }

测试一下效果:

首先我们直接访问一下/authc/**下需要登录的接口(我们没有登录,它就去调用了/pub/need_login这个接口)

提示我们需要登录才能访问,现在我们去/pub/login接口使用李四的账号登录拿到session_id,在/authc/**接口路径的heards里面加入一个键值对,k=token,value=seesion_id,继续访问(成功)

访问一下需要admin权限的接口(李四不是admin是超级管理员root,因此无法进行访问需要admin接口的权限)在headers的token中加入sessionId

使用张三的账号登录(成功)

到此我们权限这个已经成功了,但是同时也暴露出了一个问题,李四作为root权限拥有者应该有张三admin权限的所有权限。接下来我们将来解决一下这个问题(自定义一个自己的ShiroFilter)

首先我解释一下造成以上现象的原因。我们看到ShiroConfig中这一行代码

filterChainDefinitionMap.put("/admin/**","roles[admin]");

roles[admin],我们在这指定的admin权限才能够访问,其实它是支持多条权限的,我们现在将他设置为roles[admin,root]用逗号隔开。在试一次。

在这里我就不截结果图了,你会发现现在不管是张三admin还是李四root权限都无法访问/admin/**下面所有的接口了。都会提示权限不足。

因为在它的内部当我们添加多条权限的时候,它是一个&&的关系。也就是说这个用户必须同时具有root权限和admin权限才可以访问。

贴一张源码:大家看到 return 那一行就会明白,用户体必须有用roles集合里的全部权限才会返回true

public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
        Subject subject = this.getSubject(request, response);
        String[] rolesArray = (String[])((String[])mappedValue);
        if (rolesArray != null && rolesArray.length != 0) {
            Set<String> roles = CollectionUtils.asSet(rolesArray);
            return subject.hasAllRoles(roles);
        } else {
            return true;
        }
    }

我们现在自定义个CustomRolesOrAuthorizationFilter继承AuthorizationFilter实现isAccessAllowed方法:因此我们重写这个方法,并顺着它的思路稍微改进即可。

public class CustomRolesOrAuthorizationFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);

        //获取当前访问路径所需要的的角色集合
        String[] rolesArray = (String[])o;

        //如果集合中没有角色的话,则说明可以不需要权限就可以访问
        if(rolesArray == null || rolesArray.length == 0){
            return true;
        }
        Set<String> roles = CollectionUtils.asSet(rolesArray);
        //当前Subject是roles中的任何一个都可以访问
        for(String role : roles){
            if(subject.hasRole(role)){
                return true;
            }
        }
        return false;
    }
}

写好之后不能忘记将写好的类添加到ShiroConfig中

注意箭头所指的地方,之前roles为过滤自带的id,现在我们自己写了一个规则,所以讲Map中的K作为id传入即可。

测试:使用李四和张三账号分别登录都能成功。

5.7、数据加密

shiro有自带的加密工具,我们直接使用即可

在ShiroConfig中

@Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //加密算法为md5
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //嵌套加密次数为2
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

在CustomRealm的Bean中绑定加密方法:为了方便密码登录测试,我暂且将此set方法注释起来,不然的话使用原先的测试密码登录会报错。

@Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm = new CustomRealm();
        //将数据加密操作绑定到Realm中
       // customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }

5.8、设置会话管理:创建一个CustomSessionManager继承DefaultWebSessionManager

public class CustomSessionManager extends DefaultWebSessionManager {
    private static final String AUTHORIZATION = "token";

    public CustomSessionManager(){
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response){
        //将ServletRequest转换为HTTPServletReqeust并且将token设置到Headers中
        String sessionid = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        if(sessionid != null){
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            //判断sessionid是否有效,过期等
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,sessionid);
            //标记session为有效
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
            return sessionid;
        }else {
            return super.getSessionId(request,response);
        }
    }
}

在ShiroConfig中

@Bean
    public SessionManager customSessionManager(){
        CustomSessionManager customSessionManager = new CustomSessionManager();
        /*
            设置Session超时时间,默认的时间为30分钟,在此期间内如果没有任何操作session会失效,
            注意的是,在这时间内有操作的话,会不断刷新时间
         */
        customSessionManager.setGlobalSessionTimeout(15*60*1000);
        return customSessionManager;
    }

绑定到SecurityManager中

@Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //绑定会话管理
        securityManager.setSessionManager(customSessionManager());
        //绑定自定义的Realm
        securityManager.setRealm(customRealm());
        return securityManager;
    }

5.9、整合Redis

在ShiroConfig中

/**
     * 配置redis
     */
    @Bean
    public RedisManager redisManager (){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);

        return redisManager;
    }

    /**
     * 配置rediscache实现类
     */
    @Bean
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        //绑定配置
        redisCacheManager.setRedisManager(redisManager());
        //设置缓存时间为20S
        redisCacheManager.setExpire(20);

        return redisCacheManager;
    }

绑定到SecurityManager中

 //将自定义RedisCache绑定到SecurityManager中
        securityManager.setCacheManager(redisCacheManager());

测试:

首先打开redis的目录,在目录执行cmd,输入命令:redis-server.exe窗口不关,在打开一个窗口输入:redis-cli.exe.

访问admin接口首先登录

在cli窗口输入keys * 可以看到存在一个shiro的数据 ttl -名称查询剩余的时候,因为我们之前设置的redis缓存时间为20S,并且你会发现在这20S期间你去使用该账号重复访问admin接口的时候只有第一次进行我们自定义CustomRealm中的授权认证方法(查看打桩信息即可).这样我们也就使用了Redis提升了项目的性能。

6.0、自定义SessionId并将Session存入Redis中

为什么将SessionId持久化:

场景:1.某用户正在编辑功能,时间花费的可能比较久。

           2.服务器遇到了未知的故障或者项目升级,需要重启,那么重启前的用户sessionId会全部失效,但是用户不知道服务器重启了。

           3.用户编辑完毕,提交的时候弹出身份过期,会大大影响体验。

解决:将SessionId持久化后,即便项目重启了,在一定的时间内也可以用之前的那份信息去登录操作。

创建一个Config用来自定义Session

public class CustomSessionIdGenerator implements SessionIdGenerator {
    @Override
    public Serializable generateId(Session session) {

        return UUID.randomUUID().toString().replace("-","");
    }
}

在ShiroConfig中

 /**
     * 将session持久化
     */
    @Bean
    public RedisSessionDAO sessionDAO(){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        //绑定Redis配置文件
        redisSessionDAO.setRedisManager(redisManager());

        //绑定自定义设置的sessionid
        redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator());
        return redisSessionDAO;
    }

绑定到SessionManager中

 @Bean
    public SessionManager customSessionManager (){
        CustomSessionManager customSessionManager = new CustomSessionManager();
        /*
            设置Session超时时间,默认的时间为30分钟,在此期间内如果没有任何操作session会失效,
            注意的是,在这时间内有操作的话,会不断刷新时间
         */
        customSessionManager.setGlobalSessionTimeout(15*60*1000);

        //配置session持久化
        customSessionManager.setSessionDAO(sessionDAO());

        return customSessionManager;
    }

测试:

可以看到又增加了一条记录。

这样即便项目重新启动了,在设置缓存时间内用户也可以根据有效的SessionId进行登录。

附上项目结构图。

到此Shiro的基本使用就告一段落了,大家有什么的问题的可以多多指正,一点一滴共同进步。

有需要整个项目的可以留言,会及时回复。

发布了17 篇原创文章 · 获赞 18 · 访问量 1036

猜你喜欢

转载自blog.csdn.net/qq_40409260/article/details/103751788