zoukankan      html  css  js  c++  java
  • shiro整合springmvc

    说明

      代码及部分相关资料根据慕课网Mark老师的视频进行整理

      其他资料:

    流程

    配置

    1. 配置web.xml整合shiro
      把shiro整合到springMVC实质上是在web.xml配置过滤器(filter),配置DelegatingFilterProxy,让其代理shiro的过滤器,对需要认证或者授权的请求路径进行过滤。
    <!--  DelegatingFilterProxy可以代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
    "filter-name"要与spring配置文件中ShiroFilterFactoryBean的id一致;
    这里相当于把shiro和springmvc整合到一起-->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    1. 配置spring.xml添加shiro组件
      与上一节的程序一样,需要添加SecutiryManager、Realm两个核心组件。
    • (非必需)创建HashedCredentialsMatcher。用于加密,也可不加密,根据自己需求进行配置,建议加密。
    <!--  1.配置用于密码解密的HashedCredentialMatcher  -->
    <bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashIterations" value="3"/>
        <property name="hashAlgorithmName" value="MD5"/>
    </bean>
    
    • 创建realm。此处示例使用自定义的可加密MyEncryptedRealm,引用HashedCredentialMatcher
    <!--  2.配置Realm,使用自定义的MyEncryptedRealm,引用HashedCredentialMatcher  -->
    <bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
        <property name="credentialsMatcher" ref="matcher"/>
    </bean>
    
    • 创建SecurityManager。示例使用DefaultWebSecurityManager,引用上面的realm。
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="realm"/>
    </bean>
    
    • 创建ShiroFilterFactoryBean。该Bean会根据配置,生成一个被DelegatingFilterProxy代理的,类型为SpringShiroFilter的过滤器,这个过滤器包含FilterChain,用于对请求进行实际上的更详细的过滤。该Bean的id必须与web.xml中配置的DelegatingFilterProxy的“filter-name”一致。
    <!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
    该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="login.html"/>
        <property name="unauthorizedUrl" value="403.html"/>
        <!--  ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链  -->
        <property name="filterChainDefinitions">
            <value>
                /login.html = anon
                /subLogin = anon
                /register = anon
                /addPermissions = anon
                /* = authc
            </value>
        </property>
    </bean>
    

    filterChain从上到下匹配,当匹配到合适的规则时进行处理,不管后面的规则如何,所以一定要注意顺序。 value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值。
    anon:它对应的过滤器里面是空的,什么都没做;
    authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    shiro包含11个过滤器,具体信息可查看shiro官网

    实战1

    maven依赖

        <!-- springmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <!-- shiro相关 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- 日志相关 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    

    工程结构

    配置文件

    web.xml:

       <!DOCTYPE web-app PUBLIC
           "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
           "http://java.sun.com/dtd/web-app_2_3.dtd" >
    
       <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns="http://java.sun.com/xml/ns/javaee"
            xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
            version="3.0"
            metadata-complete="true">
    
       <!--  声明应用范围(整个WEB项目)内的上下文初始化参数。  -->
       <context-param>
           <param-name>contextConfigLocation</param-name>
           <!--扫描所有spring配置文件,不用在配置文件里import-->
           <param-value>classpath*:spring/spring*</param-value>
       </context-param>
    
       <!--  配置监听器,用于springIOC -->
       <listener>
           <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
       </listener>
       <!--<listener>
           <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
       </listener>-->
    
       <!--  DelegatingFilterProxy可以代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
       "filter-name"要与spring配置文件中ShiroFilterFactoryBean的id一致;
       这里相当于把shiro和springmvc整合到一起-->
       <filter>
           <filter-name>shiroFilter</filter-name>
           <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
       </filter>
       <filter-mapping>
           <filter-name>shiroFilter</filter-name>
           <url-pattern>/*</url-pattern>
       </filter-mapping>
    
       <!--  将请求路由到相应的handler  -->
       <servlet>
           <servlet-name>spring-mvc</servlet-name>
           <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
           <init-param>
               <param-name>contextConfigLocation</param-name>
               <param-value>classpath*:spring/spring-mvc.xml</param-value>
           </init-param>
           <load-on-startup>1</load-on-startup>
       </servlet>
       <servlet-mapping>
           <servlet-name>spring-mvc</servlet-name>
           <url-pattern>/</url-pattern>
       </servlet-mapping>
       </web-app>
    

    spring.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation=
           "http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--  1.配置用于密码解密的HashedCredentialMatcher  -->
        <bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashIterations" value="3"/>
            <property name="hashAlgorithmName" value="MD5"/>
        </bean>
    
        <!--  2.配置Realm,使用自定义的MyEncryptedRealm,引用HashedCredentialMatcher  -->
        <bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
            <property name="credentialsMatcher" ref="matcher"/>
        </bean>
    
        <!--  3.配置SecurityManager,引用Realm  -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <property name="realm" ref="realm"/>
        </bean>
    
        <!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
        该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <property name="securityManager" ref="securityManager"/>
            <property name="loginUrl" value="login.html"/>
            <property name="unauthorizedUrl" value="403.html"/>
            <!--  ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链  -->
            <property name="filterChainDefinitions">
                <value>
                    /login.html = anon
                    /subLogin = anon
                    /register = anon
                    /addPermissions = anon
                    /* = authc
                </value>
            </property>
        </bean>
        </beans>
    

    springmvc.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <!--  扫描shiro包下所有组件(包括@Controller、@Component等)  -->
        <context:component-scan base-package="com.lifeofcoding.shiro"></context:component-scan>
    
        <!-- 1.开启注解;
             2.注册HandlerMapping和HandlerAdapter的实现类。
             配置该参数,spring可以通过context:component-scan/标签的配置,自动将扫描到的@Component,@Controller,@Service,@Repository等注解标记的组件注册到工厂中,来处理请求。
             该参数还支持以下功能:
             a:默认提供的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
             b:xml,json的默认读写支持-->
        <mvc:annotation-driven/>
    
        <!-- 处理静态资源 -->
        <mvc:resources mapping="/*" location="/"/>
        </beans>
    

    log4j.properties:

    # Global logging configuration #u5728u5f00u53d1u73afu5883u4e0bu65e5u5fd7u7ea7u522bu8981u8bbeu7f6eu6210DEBUGuff0cu751fu4ea7u73afu5883u8bbeu7f6eu6210infou6216error
    log4j.rootLogger=DEBUG, stdout
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    

    后端代码

    com.lifeofcoding.shiro.pojo.User.java
    
    package com.lifeofcoding.shiro.pojo;
    

    import java.util.Set;

    public class User {

    private String username;
    private String password;
    private Set<String> roles;
    
    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 Set<String> getRoles() {
        return roles;
    }
    
    public void setRoles(Set<String> roles) {
        this.roles = roles;
    }
    

    }

    com.lifeofcoding.shiro.realm.MyEncryptedRealm.java
    
    package com.lifeofcoding.shiro.realm;
    

    import com.lifeofcoding.shiro.pojo.User;
    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.crypto.hash.SimpleHash;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.util.ByteSource;
    import org.apache.shiro.util.CollectionUtils;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;

    public class MyEncryptedRealm extends AuthorizingRealm {

    /** 加密次数 */
    private int iterations;
    /** 算法名 */
    private String algorithmName;
    
    /** 存储用户名和密码 */
    private final Map<String,String> userMap;
    /** 存储用户及其对应的角色 */
    private final Map<String, Set<String>> roleMap;
    /** 存储所有角色以及角色对应的权限 */
    private final Map<String,Set<String>> permissionMap;
    /** 存储用户盐值 */
    private Map<String,String> saltMap;
    
    {
        //设置Realm名,可用于获取该realm
        super.setName("MyRealm");
    }
    
    public MyEncryptedRealm(){
        this.iterations = 0;
        this.algorithmName = "MD5";
        this.userMap = new ConcurrentHashMap<>(16);
        this.roleMap = new ConcurrentHashMap<>(16);
        this.permissionMap  = new ConcurrentHashMap<>(16);
        this.saltMap = new ConcurrentHashMap<>(16);
    }
    
    /**
     * 身份认证必须实现的方法
     * @param authenticationToken token to do authenticate
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取主体中的用户名
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名获取密码,getPasswordByName自定义实现
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //3.构建authenticationInfo认证信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        //添加盐值
        String salt = getSaltByUsername(userName);
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return authenticationInfo;
    }
    
    /**
     * 用于授权,必须实现
     * @param principalCollection principals
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.获取角色信息,getRoleByUserName自定义
        Set<String> roles = getRolesByUserName(userName);
        //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
        Set<String> permissions = getPermissionsByUserName(userName);
        //4.构建认证信息并返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
    
    /**
     * 往realm添加账号信息
     * @param user user
     */
    public void addAccount(User user) throws UserExistException {
        String userName = user.getUsername();
        String password = user.getPassword();
        Set<String> roles = user.getRoles();
        if(null != userMap.get(userName)){
            throw new UserExistException("user ""+ userName +"" already exist");
        }
        //如果设置的加密次数大于0,则对密码进行加密
        if(iterations > 0){
            //此处用随机数作为盐值,可改为UUID或其它
            String salt = String.valueOf(Math.random()*10);
            saltMap.put(userName,salt);
            password = doHash(password, salt);
        }
        userMap.put(userName, password);
        //如果roles不为空,存入roleMap
        if (!CollectionUtils.isEmpty(roles)){
            roleMap.put(userName, roles);
        }
    }
    
    /**
     * 自定义部分,通过用户名获取权限信息
     * @param userName username
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getPermissionsByUserName(String userName) {
        //1.先通过用户名获取角色信息
        Set<String> roles = getRolesByUserName(userName);
        if (CollectionUtils.isEmpty(roles)){
            return null;
        }
        //2.通过角色信息获取对应的权限
        Set<String> permissions = new HashSet<>();
        roles.forEach(role -> {
            if (null != permissionMap.get(role)) {
                permissions.addAll(permissionMap.get(role));
            }
        });
        return permissions;
    }
    
    /**
     * 自定义部分,通过用户名获取密码,可改为数据库操作
     * @param userName username
     * @return java.lang.String
     */
    private String getPasswordByUserName(String userName){
        return userMap.get(userName);
    }
    
    /**
     * 自定义部分,通过用户名获取角色信息,可改为数据库操作
     * @param userName username
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getRolesByUserName(String userName){
        return roleMap.get(userName);
    }
    
    /**
     * 自定义部分,通过用户名获取角色信息,可改为数据库操作
     * @param userName username
     * @return java.util.Set<java.lang.String>
     */
    private String getSaltByUsername(String userName) {
        return saltMap.get(userName);
    }
    
    /**
     * 往realm删除账号信息
     * @param userName username
     */
    public void deleteAccount(String userName){
        userMap.remove(userName);
        roleMap.remove(userName);
    }
    
    /**
     * 添加角色权限,变参不传值会接收到长度为0的数组。
     * @param roleName name of the role
     * @param permissions permissions which this role preserve
     */
    public void addPermissions(String roleName,Set<String> permissions){
        permissionMap.put(roleName, permissions);
    }
    
    /**
     * 设置加密次数
     * @param iterations iterations to doHash
     */
    public void setHashIterations(int iterations){
        this.iterations = iterations;
    }
    
    /**
     * 设置算法名
     * @param algorithmName name of the algorithm to use
     */
    public void setAlgorithmName(String algorithmName){
        this.algorithmName = algorithmName;
    }
    
    /**
     * 计算哈希值
     * @param str str to doHash
     * @param salt user's salt
     */
    private String doHash(String str,String salt){
        salt = null==salt ? "" : salt;
        return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString();
    }
    
    /**
     * 注册时,用户已存在的异常
     */
    public class UserExistException extends Exception{
        private UserExistException(String message) {super(message);}
    }
    

    }

    com.lifeofcoding.shiro.controller.UserController.java
    
    package com.lifeofcoding.shiro.controller;
    

    import com.lifeofcoding.shiro.pojo.User;
    import com.lifeofcoding.shiro.realm.MyEncryptedRealm;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.util.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;

    @Controller
    public class UserController {

    @Autowired
    private MyEncryptedRealm realm;
    
    /**
     * 用户登录
     * @param user 用户信息,包括用户名(username)和密码(password)
     * api示例: POST /subLogin?username=java&password=123
     * */
    @ResponseBody
    @RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
    public String subLogin(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
        try {
            subject.login(token);
        }catch (Exception e){
            return e.getMessage();
        }
        return """+subject.getPrincipal().toString()+"""+"登陆成功";
    }
    
    /**
     * 用户注册
     * @param user 用户信息,包括:用户名(username)、密码(password)、角色(roles)(可选)
     * @return 返回注册信息
     * api示例: POST /register?username=java&password=123&roles=admin&roles=user  可通过指定多个roles传入roles数组
     * */
    @ResponseBody
    @RequestMapping(value = "/register",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
    public String register(User user){
        //配置realm设置加密方式
        realm.setAlgorithmName("MD5");
        //加密次数
        realm.setHashIterations(3);
        //添加账号
        try {
            realm.addAccount(user);
        }catch (Exception e){
            return e.getMessage();
        }
        return "Add account "" + user.getUsername() + "" succeeded";
    }
    
    /**
     * 测试已登录的用户是否拥有某角色
     * @param role 角色名
     * @return 返回信息
     * api示例: GET /testRole?role=admin
     * */
    @ResponseBody
    @RequestMapping(value = "testRole",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testRole(String role){
        if (null == role){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.hasRole(role)){
            return "user "" + subject.getPrincipal()+"" has role "" + role +""";
        }
        return  "user "" + subject.getPrincipal()+"" do not have role "" + role + """;
    }
    
    /**
     * 测试已登录的用户是否拥有某权限
     * @param permission 权限
     * @return 返回信息
     * api示例: GET /testPermission?permission=user:delete
     * */
    @ResponseBody
    @RequestMapping(value = "testPermission",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String testPermission(String permission){
        if (null == permission){
            return "no input";
        }
        Subject subject = SecurityUtils.getSubject();
        if (subject.isPermitted(permission)){
            return "user "" + subject.getPrincipal()+"" has permission "" + permission +""";
        }
        return  "user "" + subject.getPrincipal()+"" do not have permission "" + permission + """;
    }
    
    /**
     * 添加权限
     * api示例: GET /addPermissions?role=admin&permissions=user:delete&permissions=user:modify
     * */
    @ResponseBody
    @RequestMapping(value = "addPermissions",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
    public String addPermissions(String role, String...permissions){
        if (role==null || CollectionUtils.isEmpty(CollectionUtils.asSet(permissions))){
            return "rolename or permissions can not be empty";
        }
        realm.addPermissions(role, CollectionUtils.asSet(permissions));
        return null;
    }
    

    }

    前端代码

    login.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <form action="subLogin" method="post">
        用户名: <input type="text" name="username"/></br>
        密码: <input type="password" name="password"/></br>
        <input type="submit" value="登录">
    </form>
    
    </body>
    </html>
    

    实战2——自定义jdbcRealm

    代码与实战1基本一致,仅仅是修改Realm,改为从数据库中获取信息,再修改相关配置。

    maven依赖

        <dependencies>
        <!--  shiro  -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--  spring  -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <!--  日志相关  -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
        <!--  数据库相关  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.5.RELEASE</version>
        </dependency>
        <!-- AOP相关 aspectjweaver(用于切入点表达式)包含aspectjrt(用于aop相关注解),因此只引入前者-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.10</version>
        </dependency>
        </dependencies>
    

    项目结构

    配置文件

    spring.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation=
           "http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--  1.配置用于密码解密的CredentialMatcher  -->
        <bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
            <property name="hashIterations" value="3"/>
            <property name="hashAlgorithmName" value="MD5"/>
        </bean>
    
        <!--  2.配置Realm,使用自定义的MyEncryptedJdbcRealm,引用Matcher  -->
        <bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedJdbcRealm">
            <property name="credentialsMatcher" ref="matcher"/>
        </bean>
    
        <!--  3.配置SecurityManager,引用Realm  -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <property name="realm" ref="realm"/>
        </bean>
    
        <!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
        该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <property name="securityManager" ref="securityManager"/>
            <property name="loginUrl" value="login.html"/>
            <property name="unauthorizedUrl" value="403.html"/>
            <!--  ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链  -->
            <property name="filterChainDefinitions">
                <value>
                    /login.html = anon
                    /subLogin = anon
                    /register = anon
                    /addPermissions = anon
                    /testPermission = anon
                    /testRole = anon
                    /* = authc
                </value>
            </property>
        </bean>
        </beans>
    

    spring-dao.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation=
                   "http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
     		http://www.springframework.org/schema/tx
     		http://www.springframework.org/schema/tx/spring-tx.xsd
     		http://www.springframework.org/schema/aop
     		http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 配置数据源dataSource -->
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="username" value="root"/>
            <property name="password" value="0113"/>
            <property name="url" value="jdbc:mysql://localhost:3306/shiro"/>
        </bean>
    
        <!-- 配置JdbcTemplate,引用dataSource -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!-- 配置事务管理器transactionManager,引用dataSource -->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!-- spring-tx模块以AOP方式管理spring中的事务 -->
        <!-- 配置AOP全局事务,设置通知(Advice)的属性 -->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <tx:attributes>
                <tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
            </tx:attributes>
        </tx:advice>
    
        <!-- 配置AOP全局事务,设置切面(Aspect),引入txAdvice -->
        <aop:config proxy-target-class="true">
            <!--  对realm包下,以"add"和"delete"开头的方法开启事务 -->
            <aop:advisor advice-ref="txAdvice"
                         pointcut="execution(* com.lifeofcoding.shiro.realm..*.add*(..))
                               or execution(* com.lifeofcoding.shiro.realm..*.delete*(..))"/>
        </aop:config>
        </beans>
    

    spring-mvc.xml:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    
        <!--  扫描shiro包下所有组件(包括@Controller、@Component等)  -->
        <context:component-scan base-package="com.lifeofcoding.shiro"/>
    
        <!-- 1.开启注解;
             2.注册HandlerMapping和HandlerAdapter的实现类。
             配置该参数,spring可以通过context:component-scan/标签的配置,自动将扫描到的@Component,@Controller,@Service,@Repository等注解标记的组件注册到工厂中,来处理请求。
             该参数还支持以下功能:
             a:默认提供的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
             b:xml,json的默认读写支持-->
        <mvc:annotation-driven/>
    
        <!-- 处理静态资源 -->
        <mvc:resources mapping="/*" location="/"/>
        </beans>
    

    log4j.properties:

    # Global logging configuration #u5728u5f00u53d1u73afu5883u4e0bu65e5u5fd7u7ea7u522bu8981u8bbeu7f6eu6210DEBUGuff0cu751fu4ea7u73afu5883u8bbeu7f6eu6210infou6216error
    log4j.rootLogger=DEBUG, stdout
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    

    后台代码

    UserDaoImpl.java
    
    

    package com.lifeofcoding.shiro.dao.impl;

    import com.lifeofcoding.shiro.dao.UserDao;
    import com.lifeofcoding.shiro.pojo.User;
    import org.apache.shiro.util.CollectionUtils;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;

    @Component
    public class UserDaoImpl implements UserDao {

    @Resource
    private JdbcTemplate jdbcTemplate;
    
    @Override
    public String getPasswordByUserName(String userName) {
        String queryPasswordSql = "SELECT password FROM shiro_web_users WHERE username = ?";
        List<String> passwords = jdbcTemplate.query(queryPasswordSql, new String[]{userName}, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("password");
            }
        });
        if(CollectionUtils.isEmpty(passwords)){
            return null;
        }
        return passwords.get(0);
    }
    
    @Override
    public Set<String> getRolesByUserName(String userName) {
        String queryRoleSql = "SELECT role FROM shiro_web_user_roles WHERE username = ?";
        List<String> roles = jdbcTemplate.query(queryRoleSql, new String[]{userName}, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("role");
            }
        });
        if (CollectionUtils.isEmpty(roles)){
            return null;
        }
        return new HashSet<>(roles);
    }
    
    @Override
    public String getSaltByUserName(String userName) {
        String querySaltSql = "SELECT salt FROM shiro_web_users WHERE username = ?";
        List<String> salts = jdbcTemplate.query(querySaltSql,new String[]{userName},new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("salt");
            }
        });
        if (CollectionUtils.isEmpty(salts)){
            return null;
        }
        return salts.get(0);
    }
    
    @Override
    public void addUser(User user) throws Exception{
        if (user == null){
            return;
        }
        String addUserSql = "INSERT INTO shiro_web_users (username,password,salt) VALUES (?,?,?)";
        jdbcTemplate.update(addUserSql,new Object[]{user.getUsername(),user.getPassword(),user.getSalt()});
    }
    
    @Override
    public void deleteUser(String userName) {
        String deleteUserSql = "DELETE FROM shiro_web_users WHERE username = ?";
        jdbcTemplate.update(deleteUserSql,userName);
    }
    }
    

    PermissionDaoImpl.java
    package com.lifeofcoding.shiro.dao.impl;
    

    import com.lifeofcoding.shiro.dao.PermissionDao;
    import org.apache.shiro.util.CollectionUtils;
    import org.springframework.jdbc.core.BatchPreparedStatementSetter;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;

    @Component
    public class PermissionDaoImpl implements PermissionDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public void addPermissions(String roleName, Set<String> permissions) {
        String addPermissionSql = "INSERT IGNORE INTO shiro_web_roles_permissions (role,permission) VALUES (?,?)";
        //去掉空数据
        permissions.remove("");
        //后面StatementSetter需要用index遍历集合,所以转为List
        ArrayList<String> tempPermissions = new ArrayList<>(permissions);
        //批量添加数据
        jdbcTemplate.batchUpdate(addPermissionSql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, roleName);
                ps.setString(2, tempPermissions.get(i));
            }
    
            @Override
            public int getBatchSize() {
                return tempPermissions.size();
            }
        });
    }
    
    @Override
    public Set<String> getPermissionsByRole(String role) {
        String queryPermissionSql = "SELECT permission FROM shiro_web_roles_permissions WHERE role = ?";
        List<String> permissions = jdbcTemplate.query(queryPermissionSql, new String[]{role}, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("permission");
            }
        });
        if (CollectionUtils.isEmpty(permissions)){
            return null;
        }
        return new HashSet<>(permissions);
    }
    
    @Override
    public void deletePermissionsByRole(String role) {
        String deletePermissionsByRoleSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ?";
        jdbcTemplate.update(deletePermissionsByRoleSql,role);
    }
    
    @Override
    public void deletePermission(String permission) {
        String deletePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE permission = ?";
        jdbcTemplate.update(deletePermissionSql,permission);
    }
    
    @Override
    public void deleteRolePermission(String role, String permission) {
        String deleteRolePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ? AND permission = ?";
        jdbcTemplate.update(deleteRolePermissionSql,new Object[]{role,permission});
    }
    

    }

    RoleDaoImpl.java
    package com.lifeofcoding.shiro.dao.impl;
    

    import com.lifeofcoding.shiro.dao.RoleDao;
    import org.springframework.jdbc.core.BatchPreparedStatementSetter;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.Set;

    @Component
    public class RoleDaoImpl implements RoleDao {

    @Resource
    private JdbcTemplate jdbcTemplate;
    
    @Override
    public void addRole(String username, Set<String> roles) {
        //去掉空数据
        roles.remove("");
        String addRoleSql = "INSERT IGNORE INTO shiro_web_user_roles (username,role) VALUES (?,?)";
        //StatementSetter用index遍历集合,转为List
        ArrayList<String> tempRoles = new ArrayList<>(roles);
        //批量添加数据
        jdbcTemplate.batchUpdate(addRoleSql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ps.setString(1, username);
                ps.setString(2, tempRoles.get(i));
            }
    
            @Override
            public int getBatchSize() {
                return tempRoles.size();
            }
        });
    }
    
    @Override
    public void deleteRolesByUsername(String userName) {
        String deleteRoleByUsernameSql = "DELETE FROM shiro_web_user_roles WHERE username = ?";
        jdbcTemplate.update(deleteRoleByUsernameSql,userName);
    }
    
    @Override
    public void deleteRole(String role) {
        String deleteRoleSql = "DELETE FROM shiro_web_user_roles WHERE role = ?";
        jdbcTemplate.update(deleteRoleSql,role);
    }
    
    @Override
    public void deleteUserRole(String userName, String role) {
        String deleteUserRoleSql = "DELETE FROM shiro_web_user_roles WHERE username = ? AND role = ?";
        jdbcTemplate.update(deleteUserRoleSql,new Object[]{userName,role});
    }
    

    }

    MyEncryptedJdbcRealm.java
    package com.lifeofcoding.shiro.realm;
    

    import com.lifeofcoding.shiro.dao.PermissionDao;
    import com.lifeofcoding.shiro.dao.RoleDao;
    import com.lifeofcoding.shiro.dao.UserDao;
    import com.lifeofcoding.shiro.pojo.User;
    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.crypto.hash.SimpleHash;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.util.ByteSource;
    import org.apache.shiro.util.CollectionUtils;
    import javax.annotation.Resource;
    import java.util.HashSet;
    import java.util.Set;

    public class MyEncryptedJdbcRealm extends AuthorizingRealm {
    @Resource
    private UserDao userDao;
    @Resource
    private PermissionDao permissionDao;
    @Resource
    private RoleDao roleDao;

    /**加密次数*/
    private int iterations;
    /**加密算法名*/
    private String algorithmName;
    
    /*---------------------------------实现自定义Realm需要重写的两个方法------------------------------------*/
    
    /**
     * 身份认证必须实现的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取主体中的用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名获取密码,getPasswordByName自定义实现
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //3.如果密码不为空,则构建authenticationInfo认证信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        String salt = getSaltByUserName(userName);
        //4.认证信息添加盐值
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
        return authenticationInfo;
    }
    
    /**
     * 用于授权,必须实现
     * @param principalCollection principal的集合
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.获取角色信息,getRoleByUserName自定义
        Set<String> roles = getRolesByUserName(userName);
        //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
        Set<String> permissions = getPermissionsByUserName(userName);
        //4.构建认证信息并返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加权限信息
        simpleAuthorizationInfo.setStringPermissions(permissions);
        //添加角色信息
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
    
    //类加载时初始化
    {
        //设置Realm名,可用于获取该realm
        super.setName("MyJdbcRealm");
    }
    
    /**构造方法,初始化哈希次数及算法名称*/
    MyEncryptedJdbcRealm(){
        iterations = 0;
        algorithmName = "MD5";
    }
    
    /*--------------------------------------自定义部分--------------------------*/
    
    /**
     * 自定义部分,通过用户名获取权限信息
     * @param userName username
     * @return 该用户拥有的所有权限
     */
    public Set<String> getPermissionsByUserName(String userName) {
        //1.先通过用户名获取所有角色信息
        Set<String> roles = userDao.getRolesByUserName(userName);
        //2.通过角色信息获取对应的权限
        Set<String> permissions = new HashSet<>();
        roles.forEach(role -> {
            Set<String> tempPermissions = permissionDao.getPermissionsByRole(role);
            if (null != tempPermissions) {
                permissions.addAll(tempPermissions);
            }
        });
        return permissions;
    }
    
    /**
     * 自定义部分,通过用户名获取密码
     * @param userName username
     * @return java.lang.String
     */
    public String getPasswordByUserName(String userName){
        return userDao.getPasswordByUserName(userName);
    }
    
    /**
     * 自定义部分,通过用户名获取盐
     * @param userName username
     * @return java.lang.String
     */
    public String getSaltByUserName(String userName){
        return userDao.getSaltByUserName(userName);
    }
    
    /**
     * 自定义部分,通过用户名获取角色信息
     * @param userName username
     * @return java.util.Set<java.lang.String>
     */
    public Set<String> getRolesByUserName(String userName){
        return userDao.getRolesByUserName(userName);
    }
    
    
    /**
     * 往realm添加账号信息
     * @param user user
     */
    public void addAccount(User user) throws Exception {
        String salt = "";
        String password = user.getPassword();
        String userName = user.getUsername();
        //用户信息为空抛出异常
        if (user.getUsername()==null || user.getPassword()==null){
            throw new InfoEmptyException("username or password can not be empty");
        }
        //如果用户已经注册,抛出异常
        if(null != userDao.getPasswordByUserName(userName)){
            throw new UserExistException("user ""+ userName +"" already exist");
        }
        //如果设置的加密次数大于0,则进行加密
        if(iterations > 0){
            salt = randomSalt();
            password = doHash(password, salt);
        }
        user.setPassword(password);
        user.setSalt(salt);
        userDao.addUser(user);
        if (CollectionUtils.isEmpty(user.getRoles())){
            return;
        }
        roleDao.addRole(userName,user.getRoles());
    }
    
    /**
     * 添加角色权限
     * @param roleName 角色名
     * @param permissions 该角色拥有的权限
     */
    public void addPermissions(String roleName, Set<String> permissions) throws Exception{
        permissionDao.addPermissions(roleName,permissions);
    }
    
    /**
    * 用随机数作为盐值,可改为UUID或其他
    * */
    public String randomSalt(){
        return String.valueOf(Math.random()*10);
    }
    
    /**
     * 删除账号信息
     * @param userName 用户名
     */
    public void deleteAccount(String userName) throws Exception{
        userDao.deleteUser(userName);
        roleDao.deleteRolesByUsername(userName);
    }
    
    /**
     * 设置加密次数
     * @param iterations 哈希操作的次数
     */
    public void setHashIterations(int iterations){
        this.iterations = iterations;
    }
    
    /**
     * 设置算法名
     * @param algorithmName 哈希算法名
     */
    public void setAlgorithmName(String algorithmName){
        this.algorithmName = algorithmName;
    }
    
    /**
     * 进行哈希运算
     * @param source 原来的字符
     * @param salt 盐值
     * @return 运算结果
     * */
    private String doHash(String source, String salt){
        return new SimpleHash(this.algorithmName,source,salt,this.iterations).toString();
    }
    
    /**
     * 注册时,用户已存在的异常类
     */
    public class UserExistException extends Exception{
        public UserExistException(String message) {super(message);}
    }
    
    /**
     * 用户信息为空的异常
     * */
    public class InfoEmptyException extends Exception{
        public InfoEmptyException(String message) {super(message);}
    }
    

    }

    实战3——通过注解授权

    配置

    在springmvc配置文件中添加如下配置,务必在springmvc配置文件中添加,即上面的springmvc.xml文件。

    <!-- 开启AOP -->
    <aop:config proxy-target-class="true"/>
    
    <!-- 用于管理shiro的生命周期 -->
    <bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
    <!-- 用于注解方式验证权限的通知 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>  
    

    后端代码

    直接在controller上添加注解"@RequiresRoles"或者"@RequiresPermissions",如:

    @RequiresPermissions("user:delete")
    @RequiresRoles("admin")
    @ResponseBody
    @RequestMapping(value = "testRole",method = RequestMethod.GET)
    public String testRole(){
        return "has role: admin";
    }
    

    使用拥有指定角色或者权限的用户登录,即可访问到该"testRole()"方法,否则会抛异常。
    也可以用数组传多个参数进行授权,如:

    @RequiresPermissions({"user:delete","user:login"})
    @RequiresRoles({"user","admin"})
    

    当当前用户同时拥有所有指定的角色或者权限时,才能访问方法。

    实战4——redis实现session管理

    实现session管理,主要是给SecurityManager配置SessionManager,而SessionManager,需要配置用于Session增删查改的SessionDao。SessionDao继承AbstractSessionDAO抽象类,需要实现的方法有:

    • Serializable doCreate(Session session)
      存储session
    • Session doReadSession(Serializable sessionId)
      读取session
    • void update(Session session) throws UnknownSessionException
      更新session
    • void delete(Session session)
      删除session
    • Collection getActiveSessions()
      获取活跃的session

    maven

    添加redis依赖

    <!-- redis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.0.0</version>
    </dependency>
    

    后台代码

    封装jedis的增删查改操作:

    JedisUtil.java
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.ScanParams;
    import redis.clients.jedis.ScanResult;
    import java.util.HashSet;
    import java.util.Set;
    

    @Component
    public class JedisUtil {
    /**jedis连接池*/
    @Autowired
    private JedisPool jedisPool;

    /**获取资源*/
    private Jedis getResource(){
        return jedisPool.getResource();
    }
    
    /**
     * set
     * */
    public byte[] set(byte[] key, byte[] value) {
        Jedis jedis = getResource();
        try{
            jedis.set(key, value);
            return value;
        }finally {
            jedis.close();
        }
    }
    
    /**
     * 设置过期时间
     * */
    public void expire(byte[] key, int seconds) {
        Jedis jedis = getResource();
        try {
            jedis.expire(key,seconds);
        } finally {
            jedis.close();
        }
    }
    
    /**
     * 获取值
     * */
    public byte[] get(byte[] key) {
        Jedis jedis = getResource();
        try {
            return jedis.get(key);
        } finally {
            jedis.close();
        }
    }
    
    /**
     * 删除
     * */
    public void del(byte[] key) {
        Jedis jedis = getResource();
        try {
            jedis.del(key);
        } finally {
            jedis.close();
        }
    }
    
    /**
     * "keys"操作
     * */
    public Set<byte[]> keys(String pattern) {
        Jedis jedis = getResource();
        try {
            return jedis.keys((pattern).getBytes());
        } finally {
            jedis.close();
        }
    }
    
    /**
     * 使用scan获取所有匹配的keys,redis2.8+开始,加入了"scan"操作,
     * 允许每次只获取一部分数据,避免数据量大时"keys"造成阻塞
     * */
    public Set<byte[]> scan(String pattern){
        Jedis jedis = getResource();
        //初始化游标
        byte[] START_CURSOR = "0".getBytes();
        //每次要求返回的数据量
        int NUM_PER_SCAN = 50;
        try{
            //设置初始化游标
            byte[] cursor = START_CURSOR;
            //查询参数对象
            ScanParams params = new ScanParams();
            //设置匹配模式
            params.match(pattern.getBytes());
            //设置理想的每次返回的数据数量(不一定会返回这么多)
            params.count(NUM_PER_SCAN);
            //用一个HashSet来存储查找到的keys,因为结果可能会重复,所以用set去重
            Set<byte[]> keys = new HashSet<>();
            while(true) {
                /*redis的scan与单循环链表相似,每次scan操作,返回部分数据result以及下次scan操作需要的游标cursor*/
                ScanResult result = jedis.scan(cursor,params);
                //获取下次scan的游标,byte[]类型,如果是String类型,返回结果也会是String类型,需要注意。
                cursor = result.getCursorAsBytes();
                keys.addAll(result.getResult());
                //如果已经遍历完所有数据,则退出
                if(result.isCompleteIteration()) {break;}
            }
            return keys;
        }finally {
            jedis.close();
        }
    }
    

    }

    AbstractSessionDAO的子类:

    RedisSessionDao.java
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
    import org.springframework.util.CollectionUtils;
    import org.springframework.util.SerializationUtils;
    import com.lifeofcoding.utils.JedisUtil;
    import javax.annotation.Resource;
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    

    public class RedisSessionDao extends AbstractSessionDAO {

    /**封装的redis工具类*/
    @Resource
    private JedisUtil jedisUtil;
    
    /**在redis中存储的session的前缀*/
    private final String SHIRO_SESSION_PREFIX="shiro-session:";
    
    /**
     * 把传入的key(sessionId)转化为在redis中存储的统一格式的key
     * */
    private byte[] getKey(String key){
        return (SHIRO_SESSION_PREFIX+key).getBytes();
    }
    
    /**
     * 保存session到redis中
     * */
    private void saveSession(Session session){
        if (null != session && null != session.getId()) {
            //获取session的id并将其传化为指定格式
            byte[] key = getKey(session.getId().toString());
            //对session进行序列化
            byte[] value = SerializationUtils.serialize(session);
            jedisUtil.set(key, value);
            jedisUtil.expire(key, 600);
        }
    }
    
    /**
     * 把session保存到redis
     * */
    @Override
    protected Serializable doCreate(Session session) {
        //创建sessionId
        Serializable sessionId = generateSessionId(session);
        //给session绑定sessionId
        assignSessionId(session,sessionId);
        //保存session到redis中
        saveSession(session);
        return sessionId;
    }
    
    /**
     * 读取session
     * */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (null == sessionId) {
            return null;
        }
        //把sessionId转化为redis中的key的格式
        byte[] key = getKey(sessionId.toString());
        byte[] value = jedisUtil.get(key);
        //返回反序列化后的session
        return (Session) SerializationUtils.deserialize(value);
    }
    
    /**
     * 更新session
     * */
    @Override
    public void update(Session session) throws UnknownSessionException {
        saveSession(session);
    }
    
    /**
     * 删除session
     * */
    @Override
    public void delete(Session session) {
        if (null == session && null == session.getId()){
            return;
        }
        byte[] key = getKey(session.getId().toString());
        jedisUtil.del(key);
    }
    
    /**
     * 获取活跃的session
     * */
    @Override
    public Collection<Session> getActiveSessions() {
        //获取redis中存储session的所有key
        //Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX+"*");
        //可以自己改写、优化scan,用"scan"操作替代"keys",避免数据量大时阻塞。
        Set<byte[]> keys = jedisUtil.scan(SHIRO_SESSION_PREFIX+"*");
        Set<Session> sessions = new HashSet<Session>();
        if (CollectionUtils.isEmpty(keys)){
            return sessions;
        }
        for (byte[] key : keys){
            Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
            sessions.add(session);
        }
        return sessions;
    }
    

    }

    配置文件

    redis的配置文件:

    spring-redis.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- 创建连接池配置对象 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"/>
    
    <!-- 创建连接池 -->
    <bean id="jedisPool" class="redis.clients.jedis.JedisPool">
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
        <constructor-arg name="host" value="127.0.0.1"/>
        <constructor-arg name="port" value="6379"/>
        <!--<constructor-arg name="timeout" value="60000"/>-->
        <!--<constructor-arg name="password" value="123"/>-->
    </bean>
    
    实现了AbstractSessionDao抽象类后,在spring配置文件中配置该实现类,然后配置SessionManager。 ```xml ``` 然后在securityManager中配置sessionManager ```xml ```

    SessionManager优化


      使用DefaultSessionManager管理session时,session通过retrieveSession(SessionKey sessionKey)方法获取,该方法又调用retrieveSessionFromDataSource(sessionId),利用SessionDao从数据源中获取session,此处sessionDao就是之前的自己实现的RedisSessionDao,而“数据源”,就是redis。
      通过debug可以发现有时候在处理一次请求时,retrieveSession方法调用了很多次,这样就意味着访问了很多次redis,这给redis带来了不必要的压力。此时,可以重写该方法,把session存储到request中,需要获取session时,直接从request中获取,避免redis服务器不必要的开销。
      自定义SessionManager,需要继承 DefaultSessionManager的子类DefaultWebSessionManager,而不是直接继承DefaultSessionManager,否则获取到的sessionId和request为null;
    代码如下:

    CustomSessionManager.java
    package com.lifeofcoding.session;
    

    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.SessionKey;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.apache.shiro.web.session.mgt.WebSessionKey;
    import javax.servlet.ServletRequest;
    import java.io.Serializable;

    public class CustomSessionManager extends DefaultWebSessionManager {

    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException{
        //通过SessionKey获取SessionId
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        //通过SessionKey获取ServletRequest
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }
        //尝试从request中根据sessionId获取session
        if (null!=request && null!=sessionId){
            Session session = (Session) request.getAttribute(sessionId.toString());
            if (null!=session) {
                return session;
            }
        }
        /*如果request中没有session,则使用父类获取session,并保存到request中,
         父类DefaultWebSession是通过SessionDao获取session,在这里是从redis获取*/
        Session session = super.retrieveSession(sessionKey);
        if (null != request && null != sessionId){
            request.setAttribute(sessionId.toString(),session);
        }
        return session;
    }
    

    }

    自定义SessionManager后,修改配置文件,把DefaultSessionManager改为自己的SessionManager。

     <!--  使用自定义的sessionManager,减少对redis的压力  -->
    <bean id="sessionManager" class="com.lifeofcoding.session.CustomSessionManager">
        <property name="sessionDAO" ref="redisSessionDao"/>
    </bean>
    

    实战5——使用redis实现缓存管理

      在程序中,对用户权限数据的访问量是比较大的,如果每次授权,都去数据库中取数据,这是十分不理想的,可以用redis来充当缓存,缓存用户的授权数据,减轻数据库压力。

    后端代码

    1.继承Cache类,编写RedisCache,用于对redis中的缓存数据进行增删查改。Cache类实质上相当于DAO,仅仅是对缓存进行增删查改。

    RedisCache.java
    package com.lifeofcoding.shiro.cache;
    

    import com.lifeofcoding.shiro.utils.JedisUtil;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.springframework.stereotype.Component;
    import org.springframework.util.SerializationUtils;
    import javax.annotation.Resource;
    import java.util.Collection;
    import java.util.Set;

    @Component
    public class RedisCache<K,V> implements Cache<K,V> {

    @Resource
    private JedisUtil jedisUtil;
    
    /**
     * cache的前缀
     * */
    private final String CACHE_PREFIX = "shiro-cache:";
    
    private byte[] getKey(K k){
        if (k instanceof String){
            return (CACHE_PREFIX + k).getBytes();
        }
        return SerializationUtils.serialize(k);
    }
    
    @Override
    public V get(K k) throws CacheException {
        System.out.println("read cache from redis for user: "+k.toString());
        byte[] value = jedisUtil.get(getKey(k));
        if (null != value){
            return (V) SerializationUtils.deserialize(value);
        }
        return null;
    }
    
    @Override
    public V put(K k, V v) throws CacheException {
        byte[] key = getKey(k);
        byte[] value = SerializationUtils.serialize(v);
        jedisUtil.set(key,value);
        jedisUtil.expire(key,600);
        return v;
    }
    
    @Override
    public V remove(K k) throws CacheException {
        byte[] key = getKey(k);
        byte[] value = jedisUtil.get(key);
        jedisUtil.del(key);
        if (null != value){
            return (V) SerializationUtils.deserialize(value);
        }
        return null;
    }
    
    @Override
    public void clear() throws CacheException {
    
    }
    
    @Override
    public int size() {
        return 0;
    }
    
    @Override
    public Set<K> keys() {
        return null;
    }
    
    @Override
    public Collection<V> values() {
        return null;
    }
    

    }

    2.继承CacheManager,编写RedisCacheManager,用来返回cache。CacheManager只有一个方法“getCache(String var1)”,通过传入cache的名字,返回对应的cache,仅此而已。

    RedisCacheManager.java
    package com.lifeofcoding.shiro.cache;
    

    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.apache.shiro.cache.CacheManager;
    import javax.annotation.Resource;

    public class RedisCacheManager implements CacheManager {
    @Resource
    private RedisCache redisCache;

    /**
     * 该方法用来给shiro获取cache对象。
     * 参数s为cache的名称,此处只有一个cache,即RedisCache,直接返回单例的RedisCache实例即可。
     * */
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
    

    }

    <

    配置文件

    给SecurityManager配置CacheManager

    <!--  5.配置CacheManager  -->
    <bean id="cacheManager" class="com.lifeofcoding.shiro.cache.RedisCacheManager"/>
    
    <!--  6.配置SecurityManager,引用Realm、SessionManager、CacheManager  -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="realm"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="cacheManager"/>
    </bean>
    

    拓展

    把授权数据放redis,每次需要授权数据时就访问redis,这对redis的资源也造成一定浪费,可以在RedisCache中用Map等集合类,构造二级缓存,每次需要数据,直接从二级缓存中获取,如果没有数据,再从redis中取。

    实战6——RememberMe

    很多情况下,网站需要提供“记住我”的功能,可以使用shiro的CookieRememberMeManager实现。在配置方面只需在spring配置文件中添加配置即可。

    <!--  6.设置cookie名称和时间,cookie保存加密的用户信息,可在浏览器开发者工具查看   -->
    <bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <property name="name" value="rememberMeCookie"/>
        <property name="maxAge" value="600"/>
    </bean>
    
    <!--  7.设置RememberMeManager,引用cookie  -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <property name="cookie" ref="simpleCookie"/>
    </bean>
    
    <!--  8.配置SecurityManager,引用Realm、SessionManager、cacheManager和RememberMeManager  -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="realm"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>
    

    当然,也要修改User类,添加rememberMe字段,让用户自行决定是否启用该功能,同时修改UserController实现该功能。

    private boolean rememberMe;
    
    public boolean getRememberMe() {
        return rememberMe;
    }
    
    public void setRememberMe(boolean rememberMe) {
        this.rememberMe = rememberMe;
    }
    
    @ResponseBody
    @RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
    public String subLogin(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
        try {
            //设置自动登录
            token.setRememberMe(user.getRememberMe());
            subject.login(token);
        }catch (Exception e){
            return e.getMessage();
        }
        return """+subject.getPrincipal().toString()+"""+"登陆成功";
    }
    

    文件传送门

    github地址

  • 相关阅读:
    realsense d435i qt 测试
    realsense d435i 数据 测试
    realsense d435i测试
    ubuntu torch GPU yolov5
    IfcLayeredItem
    ubuntu大服务器 pytorch环境配置
    condarc内容
    realsense point cloud
    yolov5 环境配置
    pip error
  • 原文地址:https://www.cnblogs.com/life-of-coding/p/12142436.html
Copyright © 2011-2022 走看看