超詳細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的基本使用就告一段落了,大家有什麼的問題的可以多多指正,一點一滴共同進步。

有需要整個項目的可以留言,會及時回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章