Shiro Advanced and SaaS-HRM Authentication Authorization

Shiro Advanced and SaaS-HRM Authentication Authorization

Application of Shiro in SpringBoot project

Apache Shiro is a powerful, flexible, open source security framework. It cleanly handles authentication, authorization, enterprise session management, and encryption. More and more enterprises use Shiro as the security framework of the project to ensure the smooth operation of the project.

In the previous explanation, shiro was only used alone, which is convenient for students to have an intuitive and clear understanding of shiro. Let's take a look at how shiro is used in the springBoot project and other features today.

Case Description

Use springBoot to build applications and integrate shiro framework to complete user authentication and authorization.

Database Table

Basic engineering structure

Import the basic project code prepared in the data, and the operation of basic user role permissions is realized in this project. We only need to add Shiro-related operation codes to this project

Integrate Shiro

Integration dependencies of spring and shiro

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-core</artifactId>
     <version>1.3.2</version>
</dependency>

Modify login method

Authentication: identity authentication/login, to verify whether the user has the corresponding identity. Based on Shiro's authentication, Shiro needs to collect user login data and use the subject's login method to enter the realm to complete the authentication work.

    @RequestMapping(value="/login")
    public String login(String username,String password) {
        try{
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken uptoken = new UsernamePasswordToken(username,password);
            subject.login(uptoken);
            return "登录成功";
       }catch (Exception e) {
            return "用户名或密码错误";
       }
   }

Custom realm

Realm domain: Shiro obtains security data (such as users, roles, and permissions) from Realm, which means that SecurityManager needs to obtain the corresponding user from Realm for comparison to determine whether the user's identity is legal; it also needs to obtain users from Realm The corresponding role/authority is used to verify whether the user can perform operations; Realm can be regarded as a DataSource, that is, a secure data source

package cn.itcast.shiro.realm;

import cn.itcast.shiro.domain.Permission;
import cn.itcast.shiro.domain.Role;
import cn.itcast.shiro.domain.User;
import cn.itcast.shiro.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Set;

/**
 * 自定义的realm
 */
public class CustomRealm extends AuthorizingRealm {

    public void setName(String name) {
        super.setName("customRealm");
    }

    @Autowired
    private UserService userService;

    /**
     * 授权方法
     *      操作的时候,判断用户是否具有响应的权限
     *          先认证 -- 安全数据
     *          再授权 -- 根据安全数据获取用户具有的所有操作权限
     *
     *
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取已认证的用户数据
        User user = (User) principalCollection.getPrimaryPrincipal();//得到唯一的安全数据
        //2.根据用户数据获取用户的权限信息(所有角色,所有权限)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet<>();//所有角色
        Set<String> perms = new HashSet<>();//所有权限
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
            for (Permission perm : role.getPermissions()) {
                perms.add(perm.getCode());
            }
        }
        info.setStringPermissions(perms);
        info.setRoles(roles);
        return info;
    }


    /**
     * 认证方法
     *  参数:传递的用户名密码
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取登录的用户名密码(token)
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        String password = new String( upToken.getPassword());
        //2.根据用户名查询数据库
        User user = userService.findByName(username);
        //3.判断用户是否存在或者密码是否一致
        if(user != null && user.getPassword().equals(password)) {
            //4.如果一致返回安全数据
            //构造方法:安全数据,密码,realm域名
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
            return info;
        }
        //5.不一致,返回null(抛出异常)
        return null;
    }


    public static void main(String[] args) {
        System.out.println(new Md5Hash("123456","wangwu",3).toString());
    }
}

Shiro configuration

SecurityManager is the heart of the Shiro architecture, used to coordinate multiple internal components to complete the entire authentication and authorization process. For example, complete authentication and login by calling realm. Use the configuration method based on springboot to complete the assembly of SecurityManager and Realm

package cn.itcast.shiro;

import cn.itcast.shiro.realm.CustomRealm;
import cn.itcast.shiro.session.CustomSessionManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfiguration {

    //1.创建realm
    @Bean
    public CustomRealm getRealm() {
        return new CustomRealm();
    }

    //2.创建安全管理器
    @Bean
    public SecurityManager getSecurityManager(CustomRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);

        //将自定义的会话管理器注册到安全管理器中
        securityManager.setSessionManager(sessionManager());
        //将自定义的redis缓存管理器注册到安全管理器中
        securityManager.setCacheManager(cacheManager());

        return securityManager;
    }

    //3.配置shiro的过滤器工厂

    /**
     * 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //1.创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        //2.设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        //3.通用配置(跳转登录页面,为授权跳转的页面)
        filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
        filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
        //4.设置过滤器集合

        /**
         * 设置所有的过滤器:有顺序map
         *     key = 拦截的url地址
         *     value = 过滤器类型
         *
         */
        Map<String,String> filterMap = new LinkedHashMap<>();
        //filterMap.put("/user/home","anon");//当前请求地址可以匿名访问

        //具有某中权限才能访问
        //使用过滤器的形式配置请求地址的依赖权限
        //filterMap.put("/user/home","perms[user-home]"); //不具备指定的权限,跳转到setUnauthorizedUrl地址

        //使用过滤器的形式配置请求地址的依赖角色
        //filterMap.put("/user/home","roles[系统管理员]");

        filterMap.put("/user/**","authc");//当前请求地址必须认证之后可以访问

        filterFactory.setFilterChainDefinitionMap(filterMap);

        return filterFactory;
    }


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

    /**
     * 1.redis的控制器,操作redis
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        return redisManager;
    }

    /**
     * 2.sessionDao
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }

    /**
     * 3.会话管理器
     */
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * 4.缓存管理器
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }


    //开启对shior注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Filters in shiro

Filter

explain

anon

No parameters, open permissions, can be understood as anonymous users or tourists

authc

No parameters, authentication required

logout

No parameters, logout, after execution, it will directly jump to shiroFilterFactoryBean.setLoginUrl(); set

url

authcBasic

No parameter, means httpBasic authentication

user

No parameter, means that the user must exist, and no check is performed when logging in

ssl

No parameter, means a secure URL request, the protocol is https

perms[user]

Multiple parameters can be written, indicating that one or some permissions are required to pass. When there are multiple parameters, write perms["user, admin"]. When there are multiple parameters, each parameter must pass to pass.

roles[admin]

Multiple parameters can be written, which means that only one or some roles can pass. When there are multiple parameters, write roles["admin, user"]. When there are multiple parameters, each parameter must pass to pass.

rest[user]

According to the requested method, it is equivalent to perms[user:method], where method is post, get, delete, etc.

port[8081]

When the requested URL port is not 8081, jump to port 8081 of the currently accessed host HOST

Note: anon, authc, authcBasic, user are the first group of authentication filters, and perms, port, rest, roles, ssl are the second group of authorization filters. To pass the authorization filter, you must first complete the login authentication operation (that is, first You must complete the authentication before you can go to find the authorization) before you can go to the second group of authorizers (for example, to access the url that requires roles permission, if you have not logged in, it will directly jump to the url set by shiroFilterFactoryBean.setLoginUrl();)

Sao Dai understands: Shiro’s permission control is realized through a lot of filters, and there are only four commonly used filters, namely anon, authc, perms[user], roles[admin], and anon is public. There is no limit, authc means that you must log in, perms[user] means that the character user must have the character in the authority to access, roles[admin] means that the role must have the role of admin in order to access

authorized

Authorization: that is, permission verification, verifying whether an authenticated user has a certain permission; that is, judging whether the user can do things

Shiro supports filter-based authorization as well as annotation-based authorization

Configuration Based Authorization

In shiro, you can use filters to configure the request permission of the target address

        //配置请求连接过滤器配置
        //匿名访问(所有人员可以使用)
        filterMap.put("/user/home", "anon");
        //具有指定权限访问
        filterMap.put("/user/find", "perms[user-find]");
        //认证之后访问(登录之后可以访问)
        filterMap.put("/user/**", "authc");
        //具有指定角色可以访问
        filterMap.put("/user/**", "roles[系统管理员]");

Authorization is based on configuration. Once the operating user does not have the operation authority, the target address will not be executed. It will jump to the specified url connection address (that is, the path set by the code below). Therefore, it is necessary to handle unauthorized information prompts more friendly in the connection address

filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url

Annotation-based authorization

RequiresPermissions

Configured on the method, indicating that the execution of this method must have the specified permission

//查询
@RequiresPermissions(value = "user-find")
 public String find() { 
    return "查询用户成功";
}

RequiresRoles

Configured on the method, indicating that the execution of this method must have the specified role

    //查询
    @RequiresRoles(value = "系统管理员")
    public String find() {
       return "查询用户成功";
   }

Authorization based on the annotation configuration method, once the operating user does not have the operation permission, the target method will not be executed, and will throw

AuthorizationException exception. Therefore, it is necessary to do a good job in unified exception handling to complete unauthorized processing

  • To use annotations, configure a Bean in the configuration class
    //开启对shior注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

Session management in Shiro

All user session information in shiro will be controlled by Shiro. The session provided by shiro can be used in JavaSE/JavaEE environment. It does not depend on any underlying container and can be used independently. It is a complete session module. Unified session management through Shiro's session manager ( SessionManager )

What is shiro's session management

SessionManager (session manager): manage all Subject sessions including creation, maintenance, deletion, invalidation, verification, etc. SessionManager is a top-level component managed by SecurityManager

Shiro provides three default implementations:

  • DefaultSessionManager: for JavaSE environment
  • ServletContainerSessionManager (default): For the Web environment, directly use the session of the servlet container.
  • DefaultWebSessionManager (custom): It is used in the web environment and maintains the session by itself (it maintains the session by itself, directly discarding the session management of the Servlet container).

In the web program, after successful login through Shiro's Subject.login() method, the user's authentication information is actually stored in the HttpSession and verified by the following code.

    //登录成功后,打印所有session内容
     @RequestMapping(value="/show")
    public String show(HttpSession session) {
        // 获取session中所有的键值
        Enumeration<?> enumeration = session.getAttributeNames();
        // 遍历enumeration中的
        while (enumeration.hasMoreElements()) {
            // 获取session键值
            String name = enumeration.nextElement().toString();
            // 根据键值取session中的值
            Object value = session.getAttribute(name);
            // 打印结果
            System.out.println("<B>" + name + "</B>=" + value + "<br>/n");
       }
        return "查看session成功";
   }

Application Scenario Analysis

Under the distributed system or microservice architecture, user authentication is performed through a unified authentication center. With default session management, user information is only saved to one server. Then other services need to synchronize the session.

The session manager can specify how to generate and obtain the sessionId. Use sessionDao to complete the simulated session deposit, withdrawal and other operations

Unified session management of Shiro combined with redis

step analysis

build environment

Using the open source component Shiro-Redis can easily build the integration project of shiro and redis.

<dependency>
     <groupId>org.crazycake</groupId>
     <artifactId>shiro-redis</artifactId>
     <version>3.0.0</version>
</dependency>

Add redis configuration in springboot configuration file

 redis:
   host: 127.0.0.1
   port: 6379

Custom shiro session manager

package cn.itcast.shiro.session;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 自定义的sessionManager
 */
public class CustomSessionManager extends DefaultWebSessionManager {


    /**
     * 头信息中具有sessionid
     *      请求头:Authorization: sessionid
     *
     * 指定sessionId的获取方式
     */
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

        //获取请求头Authorization中的数据
        String id = WebUtils.toHttp(request).getHeader("Authorization");
        if(StringUtils.isEmpty(id)) {
            //如果没有携带,生成新的sessionId
            return super.getSessionId(request,response);
        }else{
            //返回sessionId;
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
    }
}

Configure Shiro's redis-based session management

Configure cn.itcast.shiro.ShiroConfiguration in the Shiro configuration class

  • Configure shiro's RedisManager, and use the RedisManager provided by the shiro-redis package to unify redis operations
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
//配置shiro redisManager
public RedisManager redisManager() {
    RedisManager redisManager = new RedisManager();
    redisManager.setHost(host);
    redisManager.setPort(port);
    return redisManager;
}
  • Shiro has its own local caching mechanism internally. In order to be more unified and convenient to manage, all of them are replaced by redis.
//配置Shiro的缓存管理器
//使用redis实现
public RedisCacheManager cacheManager() {
     RedisCacheManager redisCacheManager = new RedisCacheManager();
     redisCacheManager.setRedisManager(redisManager());
     return redisCacheManager;
}
  • Configure SessionDao, using redis-based sessionDao implemented by shiro-redis
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    return redisSessionDAO;
}
  • Configure the session manager and specify the dependencies of sessionDao
    /**
     * 3.会话管理器
     */
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
   }
  • Unified to SecurityManager management
    //配置安全管理器
    @Bean
    public SecurityManager securityManager(CustomRealm realm) {
        //使用默认的安全管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        //将自定义的realm交给安全管理器统一调度管理
        securityManager.setRealm(realm);
        return securityManager;
   }

Authentication and authorization in SaaS-HRM

demand analysis

Realize the unified rights management of Shiro-based SaaS platform. Our SaaS-HRM system is built based on microservices, so when using Shiro authentication, it is necessary to save the authentication information to a unified redis server to complete. In this way, each microservice can obtain public authentication information by specifying the sessionid in the cookie.

build environment

import dependencies

The parent project imports Shiro's dependencies

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.0.0</version>
</dependency>

config value object

There is no need to store too much user data in redis, it is consistent with the returned object of obtaining user information, and the AuthCachePrincipali interface needs to be implemented

@Setter
@Getter
public class ProfileResult implements Serializable,AuthCachePrincipal {
    private String mobile;
    private String username;
    private String company;
    private String companyId;
    private Map<String,Object> roles = new HashMap<>();
 //省略
}

Sao Dai understands: By adding the implementation of the authcacheprincipal interface to the profileresult class, the instance object of this class can be used as the credentials of the authentication system and cached in the authentication cache to improve authentication efficiency. At the same time, serializable means that it can be transmitted by the network or persisted in the database. To put it simply, Shiro authentication is used, that is, a security data is passed in and stored in Shiro's session during login verification. ! ! Look at the code below to know that ProfileResult is this security data! ! !

 //认证方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取用户的手机号和密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String mobile = upToken.getUsername();
        String password = new String( upToken.getPassword());
        //2.根据手机号查询用户
        User user = userService.findByMobile(mobile);
        //3.判断用户是否存在,用户密码是否和输入密码一致
        if(user != null && user.getPassword().equals(password)) {
            //4.构造安全数据并返回(安全数据:用户基本数据,权限信息 profileResult)
            ProfileResult result = null;
            if("user".equals(user.getLevel())) {
                result = new ProfileResult(user);
           }else {
                Map map = new HashMap();
                if("coAdmin".equals(user.getLevel())) {
                    map.put("enVisible","1");
               }
                List<Permission> list = permissionService.findAll(map);
                result = new ProfileResult(user,list);
           }
            //构造方法:安全数据,密码,realm域名
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(result,user.getPassword(),this.getName());
            return info;
       }
        //返回null,会抛出异常,标识用户名和密码不匹配
        return null;
   }
}

Configure an unauthenticated controller

For use in multiple microservices, configure a public unauthenticated and unauthorized Controller

@RestController
@CrossOrigin
public class ErrorController {
    //公共错误跳转
    @RequestMapping(value="autherror")
    public Result autherror(int code) {
        return code ==1?new Result(ResultCode.UNAUTHENTICATED):new Result(ResultCode.UNAUTHORISE);
   }
}

Sao Dai’s understanding: the above controller works side by side with the two lines of code in the following configuration class, that is, if the verification permission is found to have permission, it will jump to /autherror?code=1, and if the permission is insufficient, it will jump to /autherror ?code=2

//3.通用配置(跳转登录页面,未授权跳转的页面)
filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url

Custom public exception handler

package com.ihrm.common.handler;

import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义的公共异常处理器
 *      1.声明异常处理器
 *      2.对异常统一处理
 */
@ControllerAdvice
public class BaseExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result error(HttpServletRequest request, HttpServletResponse response,Exception e) {
        e.printStackTrace();
        if(e.getClass() == CommonException.class) {
            //类型转型
            CommonException ce = (CommonException) e;
            Result result = new Result(ce.getResultCode());
            return result;
        }else{
            Result result = new Result(ResultCode.SERVER_ERROR);
            return result;
        }
    }

    @ExceptionHandler(value = AuthorizationException.class)
    @ResponseBody
    public Result error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) {
        return new Result(ResultCode.UNAUTHORISE);
    }
}

Sao Dai understands: Because Shiro annotation authentication is used here, if the authentication fails, an exception will be thrown, so this exception handler needs to be used to uniformly handle these exceptions

Custom realm authorization

Create a public authentication and authorization realm under the ihrm-common module. It should be noted that this realm only processes authorization data, and the authentication method needs to be completed in the login module.

package com.ihrm.common.shiro.realm;

import com.ihrm.domain.system.response.ProfileResult;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.Set;

//公共的realm:获取安全数据,构造权限信息
public class IhrmRealm  extends AuthorizingRealm {

    public void setName(String name) {
        super.setName("ihrmRealm");
    }

    //授权方法
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取安全数据
        ProfileResult result = (ProfileResult)principalCollection.getPrimaryPrincipal();
        //2.获取权限信息
        Set<String> apisPerms = (Set<String>)result.getRoles().get("apis");
        //3.构造权限数据,返回值
        SimpleAuthorizationInfo info = new  SimpleAuthorizationInfo();
        info.setStringPermissions(apisPerms);
        return info;
    }

    //认证方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

Sao Dai's understanding: The above code can easily miss the setName method. The setname method is used to set the name of this authorizingrealm. In this implementation, setname overrides the parent class's setname method and forces the name to be set to "ihrmrealm". This name is usually used to uniquely identify the realm object (realm), and is used by the framework when calling the object.

custom session manager

The previous program used the jwt method for user authentication, and the front-end sent the token in the request header to the back-end. In order to adapt to the previous program, the method of obtaining sessionId needs to be changed in shiro. It's easy to solve. In shiro's session management, you can easily use the content in the request header as the sessionid

package com.ihrm.common.shiro.session;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {


    /**
     * 头信息中具有sessionid
     *      请求头:Authorization: sessionid
     *
     * 指定sessionId的获取方式
     */
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

        //获取请求头Authorization中的数据
        String id = WebUtils.toHttp(request).getHeader("Authorization");
        if(StringUtils.isEmpty(id)) {
            //如果没有携带,生成新的sessionId
            return super.getSessionId(request,response);
        }else{
            //请求头信息:bearer sessionid
            id = id.replaceAll("Bearer ","");
            //返回sessionId;
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
    }
}

User Authentication

Configure user login

    //用户名密码登录
    @RequestMapping(value="/login",method = RequestMethod.POST)
    public Result login(@RequestBody Map<String,String> loginMap) {
        String mobile = loginMap.get("mobile");
        String password = loginMap.get("password");
        try {
            //1.构造登录令牌 UsernamePasswordToken
            //加密密码
            password = new Md5Hash(password,mobile,3).toString();  //1.密码,盐,加密次数
            UsernamePasswordToken upToken = new UsernamePasswordToken(mobile,password);
            //2.获取subject
            Subject subject = SecurityUtils.getSubject();
            //3.调用login方法,进入realm完成认证
            subject.login(upToken);
            //4.获取sessionId
            String sessionId = (String)subject.getSession().getId();
            //5.构造返回结果
            return new Result(ResultCode.SUCCESS,sessionId);
       }catch (Exception e) {
            return new Result(ResultCode.MOBILEORPASSWORDERROR);
       }
   }

Sao Dai understands: the three parameters in new Md5Hash(password,mobile,3) are password, salt (by using the user name as the salt value), and the number of encryption times. The so-called salt is actually a string, md5 plus salt is the ciphertext composed of numbers and strings

Modify the profile method


    /**
     * 用户登录成功之后,获取用户信息
     *      1.获取用户id
     *      2.根据用户id查询用户
     *      3.构建返回值对象
     *      4.响应
     */
    @RequestMapping(value="/profile",method = RequestMethod.POST)
    public Result profile(HttpServletRequest request) throws Exception {
        //获取session中的安全数据
        Subject subject = SecurityUtils.getSubject();
        //1.subject获取所有的安全数据集合
        PrincipalCollection principals = subject.getPrincipals();
        //2.获取安全数据
        ProfileResult result = (ProfileResult)principals.getPrimaryPrincipal();

//        String userid = claims.getId();
//        //获取用户信息
//        User user = userService.findById(userid);
//        //根据不同的用户级别获取用户权限
//
//        ProfileResult result = null;
//
//        if("user".equals(user.getLevel())) {
//            result = new ProfileResult(user);
//        }else {
//            Map map = new HashMap();
//            if("coAdmin".equals(user.getLevel())) {
//                map.put("enVisible","1");
//            }
//            List<Permission> list = permissionService.findAll(map);
//            result = new ProfileResult(user,list);
//        }
        return new Result(ResultCode.SUCCESS,result);
    }

Sao Dai understands: the profile method was used for authorization before, since this operation has been implemented in UserRealm, and the authorized ProfileResult is placed in SimpleAuthenticationInfo, so here you only need to take it out and return it to the front end

shiro certification

To configure the realm domain for user login authentication, you only need to inherit the public IhrmRealm and supplement the authentication methods in it.

public class UserIhrmRealm extends IhrmRealm {
    @Override
    public void setName(String name) {
        super.setName("customRealm");
   }
    @Autowired
    private UserService userService;
    
    //认证方法
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取用户的手机号和密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String mobile = upToken.getUsername();
        String password = new String( upToken.getPassword());
        //2.根据手机号查询用户
        User user = userService.findByMobile(mobile);
        //3.判断用户是否存在,用户密码是否和输入密码一致
        if(user != null && user.getPassword().equals(password)) {
            //4.构造安全数据并返回(安全数据:用户基本数据,权限信息 profileResult)
            ProfileResult result = null;
            if("user".equals(user.getLevel())) {
                result = new ProfileResult(user);
           }else {
                Map map = new HashMap();
                if("coAdmin".equals(user.getLevel())) {
                    map.put("enVisible","1");
               }
                List<Permission> list = permissionService.findAll(map);
                result = new ProfileResult(user,list);
           }
            //构造方法:安全数据,密码,realm域名
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(result,user.getPassword(),this.getName());
            return info;
       }
        //返回null,会抛出异常,标识用户名和密码不匹配
        return null;
   }
}

Sao Dai’s understanding: Authentication is login verification. The user account information is verified by querying the database, and then all permissions of the user are encapsulated, which is the security data ProfileResult object. The three parameters of SimpleAuthenticationInfo are security data, password, and realm domain name.

 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(result,user.getPassword(),this.getName());

Sao Dai understands: the simpleauthenticationinfo class is used to represent authentication information. At construction time, it accepts three parameters:

  • result: The effective user object for authentication. Can be an actual object from a database or other data source.
  • user.getpassword(): A string representing a valid user object password. This usually comes from a database or other data source.
  • this.getname(): is a string representing the name of the realm (defined in shiro). This string is specified in the configuration file and is used during login authentication and authorization.

Therefore, the function of the statement simpleauthenticationinfo info = new simpleauthenticationinfo(result, user.getpassword(), this.getname()); is to create a simpleauthenticationinfo object info containing three parameter values, which is used to represent the authentication information of the user. Among them, result represents the identity of the authenticated user, user.getpassword() represents the password of the authenticated user, and this.getname() represents the name of the realm. This object can be used or processed by other components, methods or classes of the shiro framework.

Get session data

Use shiro in baseController to get authentication data from redis

    //使用shiro获取
    @ModelAttribute
    public void setResAnReq(HttpServletRequest request,HttpServletResponse response) {
        this.request = request;
        this.response = response;
        //获取session中的安全数据
        Subject subject = SecurityUtils.getSubject();
        //1.subject获取所有的安全数据集合
        PrincipalCollection principals = subject.getPrincipals();
        if(principals != null && !principals.isEmpty()){
            //2.获取安全数据
            ProfileResult result = (ProfileResult)principals.getPrimaryPrincipal();
            this.companyId = result.getCompanyId();
            this.companyName = result.getCompany();
       }
   }

user authorization

Configure @RequiresPermissions("API-USER-DELETE") on the interface that needs to be used

configuration

Construct shiro's configuration class

package com.ihrm.system;

import com.ihrm.common.shiro.realm.IhrmRealm;
import com.ihrm.common.shiro.session.CustomSessionManager;
import com.ihrm.system.shiro.realm.UserRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfiguration {

    //1.创建realm
    @Bean
    public IhrmRealm getRealm() {
        return new UserRealm();
    }

    //2.创建安全管理器
    @Bean
    public SecurityManager getSecurityManager(IhrmRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);

        //将自定义的会话管理器注册到安全管理器中
        securityManager.setSessionManager(sessionManager());
        //将自定义的redis缓存管理器注册到安全管理器中
        securityManager.setCacheManager(cacheManager());

        return securityManager;
    }

    //3.配置shiro的过滤器工厂

    /**
     * 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //1.创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        //2.设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        //3.通用配置(跳转登录页面,未授权跳转的页面)
        filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
        filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
        //4.设置过滤器集合
        Map<String,String> filterMap = new LinkedHashMap<>();
        //anon -- 匿名访问
        filterMap.put("/sys/login","anon");
        filterMap.put("/autherror","anon");
        //注册
        //authc -- 认证之后访问(登录)
        filterMap.put("/**","authc");
        //perms -- 具有某中权限 (使用注解配置授权)
        filterFactory.setFilterChainDefinitionMap(filterMap);

        return filterFactory;
    }


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

    /**
     * 1.redis的控制器,操作redis
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        return redisManager;
    }

    /**
     * 2.sessionDao
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        return sessionDAO;
    }

    /**
     * 3.会话管理器
     */
    public DefaultWebSessionManager sessionManager() {
        CustomSessionManager sessionManager = new CustomSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        //禁用cookie
        sessionManager.setSessionIdCookieEnabled(false);
        //禁用url重写   url;jsessionid=id
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    /**
     * 4.缓存管理器
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }




    //开启对shior注解的支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Sao Dai understands: The following code is an upward transformation. If UserRealm does not have this method, it will call the parent class. If there is, it will call UserRealm's own method. This writing should be to disassemble the Realm, one for authentication, one for authentication. used to authorize

    @Bean
    public IhrmRealm getRealm() {
        return new UserRealm();
    }

Here we need to comment out the previous Jwt interceptor configuration file! Just annotate @Configuration

Guess you like

Origin blog.csdn.net/qq_50954361/article/details/130414582