6. 1. Cloud Office - Rights Management

Yunshang Office System: Authority Management

Direct access to station B [like Silicon Valley] :
https://www.bilibili.com/video/BV1Ya411S7aT

This blog post is mainly published related to the course, and incorporates some of my own opinions and gives relevant solutions to the problems encountered in the learning process. Learn together and progress together! ! !

Congratulations to everyone who has completed the previous learning of user management, menu management, and front-end integration. This blog post note will explain in detail the key points of this project, authority management. You need to think and understand the teacher's thinking, and clarify the purpose and function of the code.

Article Directory

1. Authority management

1. Introduction to authority management

The authority functions of each system are different, each has its own business characteristics, and the design of authority management also has its own characteristics. However, no matter what kind of permission design, it can be roughly classified into three types: page permission (menu level), operation permission (button level), and data permission . The current system only explains: the control of menu permissions and button permissions.
insert image description here

1.1. Menu permissions

Menu permission is the control of the page. Only users with this permission can access this page. Users without this permission cannot access it. It is based on the entire page as the dimension, and the control of permissions is not so fine, so it is a coarse-grained permission .

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-3PNvo70c-1688007047614)(images/6. Rights management/image-20220606155704023.png)]

1.2. Button permissions

Button permission is to treat page operations as resources, such as delete operations, some people can operate and some cannot. For the backend, an operation is an interface. For the front end, the operation is often a button, which is a fine-grained permission .

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-ArpPRehl-1688007047617)(images/6.Privilege Management/image-20220606160034229.png)]

1.3. Design idea of ​​rights management

Earlier we explained user management, role management and menu management. We assign menu permissions to roles and roles to users, then users have all the permissions of roles (permissions include: menu permissions and button permissions).

Here everyone has to think about whether it is possible to implement multi-table query if the method of MyBatis-Plus packaging is still used. If not, how to perform multi-table query and link the five tables together! !

insert image description here
Next, you need to implement these two interfaces:

1. User login

2. Successful login to obtain user-related information (menu authority and button authority data, etc.) according to the token

We need to use JWT for user login, and then explain JWT.

Click the link below to clarify the difference between Cookie, Session, Token, and JWT and use
the silly and unclear Cookie, Session, Token, and JWT

2、JWT

2.1. Introduction to JWT

JWT is the abbreviation of JSON Web Token, that is, JSON Web Token, which is a self-contained token. It is a JSON-based open standard implemented for passing declarations between web application environments.

The statement of JWT is generally used to pass the authenticated user identity information between the identity provider and the service provider, so as to obtain resources from the resource server. For example, it is used for user login.

The most important function of JWT is the anti-counterfeiting function of token information.

2.2. Composition of JWT token

A JWT consists of three parts: JWT header, payload, signature hash,
and finally base64url encoding by the combination of these three parts to get JWT

Typically, a JWT looks like the following figure: the object is a very long string, and the characters are divided into three substrings by the "." delimiter.
https://jwt.io/

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-8emN18X6-1688007047618) (images/6. Rights Management/3402e929-2225-4c64-8f2e-4471b63366d0.png)]

JWT header

The JWT header is a JSON object describing the JWT metadata, typically as follows.

{
  "alg": "HS256",
  "typ": "JWT"
}

In the above code, the alg attribute indicates the algorithm used by the signature, and the default is HMAC SHA256 (written as HS256);

The typ attribute indicates the type of the token, and the JWT token is uniformly written as JWT.

Finally, use the Base64 URL algorithm to convert the above JSON object into a string and save it.

Payload

The payload part is the main content part of the JWT, and it is also a JSON object that contains the data that needs to be passed. JWT specifies seven default fields for selection.

iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{
  "name": "Helen",
  "role": "editor",
  "avatar": "helen.jpg"
}

Please note that JWT is unencrypted by default, and anyone can interpret its content, so do not build a private information field to store confidential information to prevent information leakage.

JSON objects are also converted to strings using the Base64 URL algorithm.

signature hash

The signature hash part is to sign the above two parts of the data, and generate a hash through the specified algorithm to ensure that the data will not be tampered with.

First, you need to specify a password (secret). This password is only stored on the server and cannot be disclosed to the user. Then, use the signature algorithm specified in the header (HMAC SHA256 by default) to generate a signature according to the following formula.

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)    ==>   签名hash

After calculating the signature hash, the three parts of the JWT header, payload and signature hash are combined into a string, and each part is separated by "." to form the entire JWT object.

Base64URL Algorithm

As mentioned earlier, both the JWT header and payload serialization algorithms use Base64URL. This algorithm is similar to the common Base64 algorithm, with slight differences.

A JWT as a token can be placed in the URL (eg api.example/?token=xxx). The three characters used in Base64 are "+", "/" and "=". Since they have special meanings in URLs, they are replaced in Base64URL: "=" is removed, "+" is replaced with "-", "/" is replaced with "_", this is the Base64URL algorithm.

2.3. Project integration JWT

Operation module: common-util

2.3.1. Introducing dependencies
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>
2.3.2. Add JWT helper class

Tool class for generating token

package com.atguigu.common.jwt;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;

public class JwtHelper {
    
    

    private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
    private static String tokenSignKey = "123456";

    public static String createToken(Long userId, String username) {
    
    
        String token = Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    public static Long getUserId(String token) {
    
    
        try {
    
    
            if (StringUtils.isEmpty(token)) return null;

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            Integer userId = (Integer) claims.get("userId");
            return userId.longValue();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return null;
        }
    }

    public static String getUsername(String token) {
    
    
        try {
    
    
            if (StringUtils.isEmpty(token)) return "";

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return (String) claims.get("username");
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return null;
        }
    }
	// 测试工具类的可用性
    public static void main(String[] args) {
    
    
        String token = JwtHelper.createToken(1L, "admin");
        System.out.println(token);
        System.out.println(JwtHelper.getUserId(token));
        System.out.println(JwtHelper.getUsername(token));
    }
}

3. User login

3.1. Modify the login method

Modify the login method of the IndexController class

@Autowired
private SysUserService sysUserService;
    
@ApiOperation(value = "登录")
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo) {
    
    
    SysUser sysUser = sysUserService.getByUsername(loginVo.getUsername());
    if(null == sysUser) {
    
    
        throw new GuiguException(201,"用户不存在");
    }
    if(!MD5.encrypt(loginVo.getPassword()).equals(loginVo.getPassword())) {
    
    
        throw new GuiguException(201,"密码错误");
    }
    if(sysUser.getStatus().intValue() == 0) {
    
    
        throw new GuiguException(201,"用户被禁用");
    }

    Map<String, Object> map = new HashMap<>();
    map.put("token", JwtHelper.createToken(sysUser.getId(), sysUser.getUsername()));
    return Result.ok(map);
}

3.2. Add service interface and implementation

SysUser getByUsername(String username);

Interface implementation:

@Override
public SysUser getByUsername(String username) {
    
    
   return this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
}

4. Obtain user information

Interface data:

Map<String, Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("name","admin");
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
map.put("buttons", new ArrayList<>());
map.put("routers", new ArrayList<>());

Description: Mainly to obtain the menu authority and button authority data of the currently logged in user

4.1. Obtain user menu permissions

Description: To obtain menu permission data, we need to build the menu data into a routing data structure :
insert image description here

4.1.1. Define interface

SysMenuService class

/**
 * 获取用户菜单
 * @param userId
 * @return
 */
List<RouterVo> findUserMenuList(Long userId);
4.1.2. Interface implementation
@Override
public List<RouterVo> findUserMenuList(Long userId) {
    
    
    //超级管理员admin账号id为:1
    List<SysMenu> sysMenuList = null;
    if (userId.longValue() == 1) {
    
    
        sysMenuList = this.list(new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getStatus, 1).orderByAsc(SysMenu::getSortValue));
    } else {
    
    
        sysMenuList = sysMenuMapper.findListByUserId(userId);
    }
    //构建树形数据
    List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenuList);

    List<RouterVo> routerVoList = this.buildMenus(sysMenuTreeList);
    return routerVoList;
}

/**
 * 根据菜单构建路由
 * @param menus
 * @return
 */
private List<RouterVo> buildMenus(List<SysMenu> menus) {
    
    
    List<RouterVo> routers = new LinkedList<RouterVo>();
    for (SysMenu menu : menus) {
    
    
        RouterVo router = new RouterVo();
        router.setHidden(false);
        router.setAlwaysShow(false);
        router.setPath(getRouterPath(menu));
        router.setComponent(menu.getComponent());
        router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
        List<SysMenu> children = menu.getChildren();
        //如果当前是菜单,需将按钮对应的路由加载出来,如:“角色授权”按钮对应的路由在“系统管理”下面
        if(menu.getType().intValue() == 1) {
    
    
            List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
            for (SysMenu hiddenMenu : hiddenMenuList) {
    
    
                RouterVo hiddenRouter = new RouterVo();
                hiddenRouter.setHidden(true);
                hiddenRouter.setAlwaysShow(false);
                hiddenRouter.setPath(getRouterPath(hiddenMenu));
                hiddenRouter.setComponent(hiddenMenu.getComponent());
                hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
                routers.add(hiddenRouter);
            }
        } else {
    
    
            if (!CollectionUtils.isEmpty(children)) {
    
    
                if(children.size() > 0) {
    
    
                    router.setAlwaysShow(true);
                }
                router.setChildren(buildMenus(children));
            }
        }
        routers.add(router);
    }
    return routers;
}

/**
 * 获取路由地址
 *
 * @param menu 菜单信息
 * @return 路由地址
 */
public String getRouterPath(SysMenu menu) {
    
    
    String routerPath = "/" + menu.getPath();
    if(menu.getParentId().intValue() != 0) {
    
    
        routerPath = menu.getPath();
    }
    return routerPath;
}
4.1.3. Add mapper interface

SysMenuMapper class

List<SysMenu> findListByUserId(@Param("userId") Long userId);
4.1.4. Add xml method

Create a new SysMenuMapper.xml file

Use the sql common column in advance
and then

SELECT *:表示查询结果包含两个表的所有列,即返回所有匹配的行和对应的列。 FROM m:表示查询的主要表是 m。 INNER JOIN:表示使用内连接操作符,它会返回两个表中满足连接条件的匹配行。 sys_role_menu rm:是 sys_role_menu 表的别名,用 rm 代表它。 ON rm.menu_id = m.id:是连接条件,表示连接 sys_role_menu 表的 menu_id 列和 m 表的 id 列。

This code is used to query matching rows between m table and n table where m_id and n_id columns are equal. In other words, it will return those rows with the same m_id and n_id values ​​that exist in both tables.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">


<mapper namespace="com.atguigu.system.mapper.SysMenuMapper">

   <resultMap id="sysMenuMap" type="com.atguigu.model.system.SysMenu" autoMapping="true">
   </resultMap>

   <!-- 用于select查询公用抽取的列 -->
   <sql id="columns">
      m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
   </sql>


    <select id="findListByUserId" resultMap="sysMenuMap">
      select
      distinct <include refid="columns" />
      from sys_menu m
      inner join sys_role_menu rm on rm.menu_id = m.id
      inner join sys_user_role ur on ur.role_id = rm.role_id
      where
      ur.user_id = #{userId}
      and m.status = 1
      and rm.is_deleted = 0
      and ur.is_deleted = 0
      and m.is_deleted = 0
   </select>

</mapper>
内连接(INNER JOIN)和外连接(OUTER JOIN)是关系型数据库中用于连接多个表的操作。

1. 内连接(INNER JOIN):
   内连接是连接操作中最常用的类型之一。它通过匹配两个表中的列值,返回满足连接条件的行。

   - 语法:`SELECT * FROM table1 INNER JOIN table2 ON table1.column = table2.column`
   - 示例:假设有两个表A和B,连接条件是A表的某一列与B表的某一列相等,则内连接的结果是仅包含A表和B表中共有的行的集合。

   内连接只返回两个表中匹配的行,不会返回任何不匹配的行。在连接结果中,只有那些在连接列上具有相同值的行会被包含在内。

2. 外连接(OUTER JOIN):
   外连接用于返回连接操作中,既包括匹配的行,又包括其中一个表中没有匹配的行。

   - 左外连接(LEFT OUTER JOIN):返回左表中的所有行以及与其匹配的右表中的行。
     - 语法:`SELECT * FROM table1 LEFT OUTER JOIN table2 ON table1.column = table2.column`
     - 示例:左外连接的结果包含左表A中的所有行,以及与A表中行匹配的B表中的行。如果没有匹配的行,B表中的列将被填充为 NULL。

   - 右外连接(RIGHT OUTER JOIN):返回右表中的所有行以及与其匹配的左表中的行。
     - 语法:`SELECT * FROM table1 RIGHT OUTER JOIN table2 ON table1.column = table2.column`
     - 示例:右外连接的结果包含右表B中的所有行,以及与B表中行匹配的A表中的行。如果没有匹配的行,A表中的列将被填充为 NULL。

   - 全外连接(FULL OUTER JOIN):返回左表和右表中的所有行。
     - 语法:`SELECT * FROM table1 FULL OUTER JOIN table2 ON table1.column = table2.column`
     - 示例:全外连接的结果包含左表A和右表B中的所有行。如果没有匹配的行,对应的列将被填充为 NULL。

外连接提供了更灵活的连接方式,可以返回更全面的结果集,包括匹配和不匹配的行。

There will be an error starting the back-end test because of the problem of Maven loading
https://blog.csdn.net/AN_NI_112/article/details/131352638?spm=1001.2014.3001.5502

4.2. Obtain user button permissions

Description: You only need to get the button ID

4.1.1. Define interface

SysMenuService class

/**
 * 获取用户按钮权限
 * @param userId
 * @return
 */
List<String> findUserPermsList(Long userId);
4.1.2. Interface implementation
@Override
public List<String> findUserPermsList(Long userId) {
    
    
    //超级管理员admin账号id为:1
    List<SysMenu> sysMenuList = null;
    if (userId.longValue() == 1) {
    
    
        sysMenuList = this.list(new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getStatus, 1));
    } else {
    
    
        sysMenuList = sysMenuMapper.findListByUserId(userId);
    }
    List<String> permsList = sysMenuList.stream().filter(item -> item.getType() == 2).map(item -> item.getPerms()).collect(Collectors.toList());
    return permsList;
}

4.3. Modify the Controller method

IndexController class

@ApiOperation(value = "获取用户信息")
@GetMapping("info")
public Result info(HttpServletRequest request) {
    
    
    String username = JwtHelper.getUsername(request.getHeader("token"));
    Map<String, Object> map = sysUserService.getUserInfo(username);
    return Result.ok(map);
}

4.3. Define the service interface

SysUserService class

/**
 * 根据用户名获取用户登录信息
 * @param username
 * @return
 */
Map<String, Object> getUserInfo(String username);

4.4. Implementation of service interface

@Autowired
private SysMenuService sysMenuService;
@Override
public Map<String, Object> getUserInfo(String username) {
    
    
   Map<String, Object> result = new HashMap<>();
   SysUser sysUser = this.getByUsername(username);

   //根据用户id获取菜单权限值
   List<RouterVo> routerVoList = sysMenuService.findUserMenuList(sysUser.getId());
   //根据用户id获取用户按钮权限
   List<String> permsList = sysMenuService.findUserPermsList(sysUser.getId());

   result.put("name", sysUser.getName());
   result.put("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
   //当前权限控制使用不到,我们暂时忽略
   result.put("roles",  new HashSet<>());
   result.put("buttons", permsList);
   result.put("routers", routerVoList);
   return result;
}

5. Front-end docking

Refer to the front-end docking document: "Front-end permission docking document"

Can directly import the complete code of the front-end project

You can follow the video to change it a little bit, but there may be errors:
probably P51, you can’t jump to the homepage on the login page. The routers and buttons
in the teacher’s code are reversed. Just change it in users.js like this .



insert image description here

Students who make mistakes can try it. I haven’t changed the following one. It should be the buttons on the back end first, and then the routers.
insert image description here
insert image description here

6. Summary

At present, we have realized the authority control of the front-end menu and buttons, and the server has not added any control, so how to control the server? In fact, it is very simple. It is to add corresponding permission control to the controller method corresponding to the page button, that is, to determine whether the current user has access permission before entering the controller method.

How to achieve it? If we implement it ourselves, we must think of Fillter plus Aop. Is there a ready-made open source technology framework? The answer is yes, a series of open source frameworks such as Spring Security and Shiro are available.

2. Introduction to Spring Security

1. Introduction to Spring Security

Spring is a very popular and successful Java application development framework, and Spring Security is a member of the Spring family. Based on the Spring framework, Spring Security provides a complete solution for web application security.

As you may know, the two core functions of security are " authentication " and " authorization ". Generally speaking, the security of web applications includes **user authentication (Authentication) and user authorization (Authorization)** two parts, which are also important core functions of SpringSecurity.

(1) User authentication refers to: verifying whether a user is a legitimate subject in the system, that is to say, whether the user can access the system. User authentication generally requires the user to provide a user name and password, and the system completes the authentication process by verifying the user name and password.

In layman's terms, it means whether the system thinks the user can log in

(2) User authorization refers to verifying whether a user has permission to perform an operation. In a system, different users have different permissions. For example, for a file, some users can only read it, while others can modify it. Generally speaking, the system assigns different roles to different users, and each role corresponds to a series of permissions.

In layman's terms, the system judges whether the user has permission to do certain things.

2. Comparison of the same product

3.1、Spring Security

Part of the Spring technology stack.

https://spring.io/projects/spring-security

Secure your applications by providing full and scalable authentication and authorization support.

Spring Security features:

⚫ Seamless integration with Spring.

⚫ Comprehensive permission control.

⚫ Designed specifically for web development.

​ ◼Old versions cannot be used without the Web environment.

​ ◼The new version extracts the entire framework hierarchically and divides it into core modules and Web modules. Introducing the core module alone can break away from the Web environment.

⚫ Heavyweight.

3.2、 Shiro

Apache's lightweight permission control framework.

Features:

⚫ Lightweight. The philosophy advocated by Shiro is to make complex things simple. Better performance for Internet applications with higher performance requirements.

⚫ Versatility.

​ ◼Benefits: Not limited to the Web environment, it can be used without the Web environment.

​ ◼ Defect: Some specific requirements in the Web environment require manual code customization.

Spring Security is a security management framework in the Spring family. In fact, Spring Security has been developed for many years before Spring Boot appeared, but it is not used much. The field of security management has always been dominated by Shiro.

Compared with Shiro, it is more troublesome to integrate Spring Security in SSM. Therefore, although Spring Security is more powerful than Shiro, it is not used as much as Shiro (although Shiro does not have as many functions as Spring Security, Shiro is enough for most projects). Since Spring Boot is available, Spring Boot provides an automatic configuration solution for Spring Security, and Spring Security can be used with less configuration.

3. Spring Security implements permissions

To protect Web resources, the best way is Filter.
To protect method calls, the best way is AOP .

When Spring Security performs authentication and authentication, it uses a series of Filters to intercept.

img

As shown in the figure, a request to access the API will go through the filters in the blue frame from left to right. The green part is the filter responsible for authentication, the blue part is responsible for exception handling, and the orange part is responsible for authorization . After a series of interceptions, we finally accessed our API.

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-xna7pGjO-1688007047620)(assets\image-20230206104938580.png)]

Here we only need to focus on two filters: UsernamePasswordAuthenticationFilterresponsible for login authentication and FilterSecurityInterceptorresponsible for authority authorization.

Explanation: The core logic of Spring Security is all in this set of filters, which will call various components to complete functions. If you master these filters and components, you will master Spring Security ! The way the framework is used is to extend these filters and components.

1. Getting Started with Spring Security

We do integration on the basis of existing projects, and the Spring Security permission control part is also a public module, which service module needs to be imported directly.

Subsequent our Spring Cloud microservice projects may be developed based on this permission system, so we need to do a good job of technical expansion.

1.1. Create spring-security module

Create a spring-security public module under the common module, such as: service-util module

1.2. Add dependencies

Modify pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.atguigu</groupId>
        <artifactId>common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>spring-security</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<scope>provided </scope>
		</dependency>
    </dependencies>

</project>

Description: After the dependency package (spring-boot-starter-security) is imported, Spring Security provides many functions by default to protect the entire application:

1. Require authenticated users to interact with the application

2. Create a default login form

3. Generate usera random password for the username and print it on the console

​ Wait...

1.3. Add configuration class

package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

}

1.4, service-oa module introduction

Introduce the authority module in service-oa and add the dependency to the pom.mxl file

<dependency>
    <groupId>com.atguigu</groupId>
    <artifactId>spring-security</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

1.5. Start the project test

Access in browser: http://localhost:8800/admin/system/sysRole/findAll

Automatically redirected to the login page

[External link image transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the image and upload it directly (img-Ohot7X1O-1688007047623) (images/6. Rights Management/image-20220606095319741.png)]

Default username: user

The password will be printed on the console when the project is started. Note that the password will change every time it is started!

[External link image transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the image and upload it directly (img-StBEC1cQ-1688007047625) (images/6. Rights management/image-20220607103826495.png)]

Enter the user name and password, successfully access the controller method and return data, indicating that the default security protection of Spring Security is in effect.

In actual development, these default configurations cannot meet our needs. We need to extend Spring Security components to complete custom configurations to meet our project requirements.

2. User authentication

User authentication process:

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-w1H20suI-1688007047626)(images/6.Privilege Management/image-20220620115942257.png)]

2.1. User Authentication Core Components

There will be many users in our system, and confirming which user is currently using our system is the ultimate purpose of login authentication. Here we have extracted a core concept: currently logged in user/currently authenticated user . The security of the entire system revolves around the current logged-in user. This is not difficult to understand. If the current logged-in user cannot confirm, then A places an order and it is placed on B's account, which is not a mess. The embodiment of this concept in Spring Security is that Authenticationit stores authentication information representing the currently logged in user.

How do we obtain and use it in the program? We need to SecurityContextget it through Authentication, SecurityContextwhich is our context object! This context object is SecurityContextHoldermanaged by and you can use it anywhere in the program:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolderThe principle is very simple, it is used ThreadLocalto ensure that the same object is passed in one thread!

Now we know the three core components in Spring Security:

​ 1. Authentication: Stores authentication information, representing the currently logged in user

​ 2. SeucirtyContext: Context object, used to getAuthentication

​ 3. SecurityContextHolder: Context management object, used to get anywhere in the programSecurityContext

AuthenticationWhat information is in:

​ 1. Principal: User information, usually the user name without authentication, and usually the user object after authentication

2. Credentials: User credentials, usually a password

3. Authorities: User permissions

2.2. User authentication

How does Spring Security perform user authentication?

AuthenticationManagerIt is the component used by Spring Security to perform authentication. You only need to call its authenticatemethod to complete the authentication. The default authentication method of Spring Security is UsernamePasswordAuthenticationFilterauthenticated in this filter, which is responsible for the authentication logic.

The key code for Spring Security user authentication is as follows:

// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

Let's analyze it below.
insert image description here
insert image description here

2.2.1. Authentication interface analysis

AuthenticationManagerThe validation logic is very simple:

According to the user name, the user object is first queried (throws an exception if it is not found), and the password of the user object is verified with the passed password. If the password does not match, an exception is thrown.

There is nothing to say about this logic, it couldn't be simpler. The point is that Spring Security provides components for each step here:

1. Who executes the logic of querying the user object based on the user name ? User object data can be stored in memory, in files, or in databases, and you have to determine how to check it. This part is handled by ** UserDetialsService**. This interface has only one method loadUserByUsername(String username), which is to query the user object by username. The default implementation is to query in memory.

2. What is the user object that is queried? The user object data in each system is different, we need to confirm what our user data is like. The user data in Spring Security is UserDetailsrepresented by ** **, which provides common attributes such as account number and password.

3. You may think it is relatively simple to verify the password.if、else If it is done, there is no need to use any components, right? But after all, the framework is a more comprehensive framework. In addition to if、elsesolving the problem of password encryption, this component is ** PasswordEncoder**, which is responsible for password encryption and verification.

We can look at AuthenticationManagerthe approximate source code of the verification logic:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
...省略其他代码

    // 传递过来的用户名
    String username = authentication.getName();
    // 调用UserDetailService的方法,通过用户名查询出用户对象UserDetail(查询不出来UserDetailService则会抛出异常)
    UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
    String presentedPassword = authentication.getCredentials().toString();

    // 传递过来的密码
    String password = authentication.getCredentials().toString();
    // 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配
    if (!passwordEncoder.matches(password, userDetails.getPassword())) {
    
    
        // 密码错误则抛出异常
        throw new BadCredentialsException("错误信息...");
    }

    // 注意哦,这里返回的已认证Authentication,是将整个UserDetails放进去充当Principal
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
            authentication.getCredentials(), userDetails.getAuthorities());
    return result;

...省略其他代码
}

UserDetialsService, UserDetails, PasswordEncoder, these three components, Spring Security, have default implementations, which generally cannot meet our actual needs, so here we implement these components ourselves!

Next, we will implement user authentication in the project.

2.2.3, Encryptor PasswordEncoder

Encryption Our project adopts MD5 encryption

Operation module: spring-security module

Custom encryption processing component: CustomMd5PasswordEncoder

package com.atguigu.security.custom;

import com.atguigu.common.util.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * <p>
 * 密码处理
 * </p>
 *
 */
@Component
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    
    

    public String encode(CharSequence rawPassword) {
    
    
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
    
    
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}
2.2.4, user object UserDetails

This interface is what we call the user object, which provides some general attributes of the user. The source code is as follows:

public interface UserDetails extends Serializable {
    
    
	/**
     * 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)
     */
    Collection<? extends GrantedAuthority> getAuthorities();
    /**
     * 用户密码
     */
    String getPassword();
    /**
     * 用户名
     */
    String getUsername();
    /**
     * 用户没过期返回true,反之则false
     */
    boolean isAccountNonExpired();
    /**
     * 用户没锁定返回true,反之则false
     */
    boolean isAccountNonLocked();
    /**
     * 用户凭据(通常为密码)没过期返回true,反之则false
     */
    boolean isCredentialsNonExpired();
    /**
     * 用户是启用状态返回true,反之则false
     */
    boolean isEnabled();
}

In actual development, our user attributes are various, and these default attributes may not be satisfied, so we generally implement this interface by ourselves, and then set up our actual user entity object. It is troublesome to rewrite many methods to implement this interface. We can inherit the class provided by Spring Security org.springframework.security.core.userdetails.User, which implements UserDetailsthe interface and saves us the work of rewriting methods:

Operation module: spring-security module

Add CustomUser object

package com.atguigu.security.custom;

import com.atguigu.model.system.SysUser;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

public class CustomUser extends User {
    
    

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
     */
    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
    
    
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }

    public SysUser getSysUser() {
    
    
        return sysUser;
    }

    public void setSysUser(SysUser sysUser) {
    
    
        this.sysUser = sysUser;
    }
    
}
2.2.5 Business Object UserDetailsService

The interface is simple with only one method:

public interface UserDetailsService {
    
    
    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

We implement this interface and complete our business

Operation module: service-oa

Add the UserDetailsServiceImpl class to implement the UserDetailsService interface

package com.atguigu.system.service.impl;

import com.atguigu.common.execption.GuiguException;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.model.system.SysUser;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.system.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Collections;


@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private SysUserService sysUserService;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        SysUser sysUser = sysUserService.getByUsername(username);
        if(null == sysUser) {
    
    
            throw new UsernameNotFoundException("用户名不存在!");
        }

        if(sysUser.getStatus().intValue() == 0) {
    
    
            throw new RuntimeException("账号已停用");
        }
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

AuthenticationManagerWe have already implemented the three components called by the verification!

2.2.6. User-defined user authentication interface
package com.atguigu.security.fillter;

import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.vo.system.LoginVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
 * </p>
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    

    public TokenLoginFilter(AuthenticationManager authenticationManager) {
    
    
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
    
    
        try {
    
    
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
    
    
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
    
    
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
    
    

        if(e.getCause() instanceof RuntimeException) {
    
    
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
    
    
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}

Add tool class: ResponseUtil

Add module: common-util

package com.atguigu.common.util;

import com.atguigu.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {
    
    

    public static void out(HttpServletResponse response, Result r) {
    
    
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
    
    
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}
2.2.7, authentication analysis token

Because the user login status is stored in the token in the client, each request interface carries the token in the request header, and the background intercepts and parses the token through a custom token filter to complete the authentication and fill in the user information entity.

package com.atguigu.security.fillter;

import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

/**
 * <p>
 * 认证解析token过滤器
 * </p>
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    
    

    public TokenAuthenticationFilter() {
    
    

    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
    
    
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
    
    
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
    
    
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
    
    
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
    
    
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
    
    
            String useruame = JwtHelper.getUsername(token);
            logger.info("useruame:"+useruame);
            if (!StringUtils.isEmpty(useruame)) {
    
    
                return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
            }
        }
        return null;
    }
}
2.2.8, configure user authentication

Modify the WebSecurityConfig configuration class

package com.atguigu.security.config;

import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.filter.TokenAuthenticationFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
    
    
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf跨站请求伪造
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                .antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        // 指定UserDetailService和加密器
    auth.userDetailsService(userDetailsService)
        .passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
    
    
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

illustrate:

1. We are a front-end and back-end separation project, use jwtthe generated token, that is, the user state is saved in the client, and the front-end and back-end interactions 无sessionare generated through the api interface, so we do not need to configure formLogin, and the session is disabled

2. Access in browser: http://localhost:8800/admin/system/sysRole/findAll

{
    "code": 209,
    "message": "没有权限",
    "data": null
}
2.2.9. Login through swagger test

Set a breakpoint on the corresponding custom component to see if it executes as expected.

1. Enter the correct user name and password first.

2. Enter wrong username and password

Conclusion: as expected

3. User authorization

In Spring Security, the default FilterSecurityInterceptor is used for permission verification. In the FilterSecurityInterceptor, the Authentication will be obtained from the SecurityContextHolder , and then the permission information in it will be obtained. Determine whether the current user has the required permissions to access the current resource.

Authentication class in Spring Security:

public interface Authentication extends Principal, Serializable {
    
    
	//权限数据列表
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

When the loadUserByUsername method is executed during login, return new CustomUser(sysUser, Collections.emptyList()); the following empty data connection is the permission data returned to Spring Security.

How to get permission data in TokenAuthenticationFilter? When logging in, we save the permission data in redis (the user name is key, and the permission data is value), so that the permission data can be obtained by obtaining the user name through token, so that a complete Authentication object can be formed.

3.1. Modify the loadUserByUsername interface method

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
    SysUser sysUser = sysUserService.getByUsername(username);
    if(null == sysUser) {
    
    
        throw new UsernameNotFoundException("用户名不存在!");
    }

    if(sysUser.getStatus().intValue() == 0) {
    
    
        throw new GuiguException(ResultCodeEnum.ACCOUNT_STOP);
    }
    List<String> userPermsList = sysMenuService.findUserPermsList(sysUser.getId());
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    for (String perm : userPermsList) {
    
    
        authorities.add(new SimpleGrantedAuthority(perm.trim()));
    }
    return new CustomUser(sysUser, authorities);
}

3.2, spring-security module configuration redis

add dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.3. Modify TokenLoginFilter login success method

After successful login, we will insure the authority data to reids

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    

    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
    
    
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
        this.redisTemplate = redisTemplate;
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
    
    
        try {
    
    
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
    
    
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
    
    
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        //保存权限数据
        redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
    
    

        if(e.getCause() instanceof RuntimeException) {
    
    
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
    
    
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}

3.4. Modify TokenAuthenticationFilter

Authentication is to obtain permission data from redis

Full code:

package com.atguigu.security.fillter;

import com.alibaba.fastjson.JSON;
import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 认证解析token过滤器
 * </p>
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    
    

    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
    
    
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
    
    
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
    
    
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
    
    
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
    
    
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
    
    
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
    
    
            String username = JwtHelper.getUsername(token);
            logger.info("useruame:"+username);
            if (!StringUtils.isEmpty(username)) {
    
    
                String authoritiesString = (String) redisTemplate.opsForValue().get(useruame);
                List<Map> mapList = JSON.parseArray(authoritiesString, Map.class);
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for (Map map : mapList) {
    
    
                    authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
                }
                return new UsernamePasswordAuthenticationToken(useruame, null, authorities);
                } else {
    
    
                    return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                }
            }
        }
        return null;
    }
}

3.5, modify the configuration class

Modify the WebSecurityConfig class

Add annotations to the configuration class:

Enable the method-based security authentication mechanism, that is to say, enable the security confirmation of the annotation mechanism in the controller of the web layer

@EnableGlobalMethodSecurity(prePostEnabled = true)

Add injection bean:

@Autowired
private RedisTemplate redisTemplate;

Add parameters:

Add a redisTemplate parameter with a fillter

The complete code is as follows:

package com.atguigu.security.config;

import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.fillter.TokenAuthenticationFilter;
import com.atguigu.security.fillter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;

    @Autowired
    private RedisTemplate redisTemplate;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
    
    
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                //.antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
    
    
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

3.6, service-oa module adds redis configuration

application-dev.yml configuration file

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 1800000
    password:
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲

3.7. Control the controller layer interface permissions

Spring Security disables annotations by default. To enable annotations, you need to add the @EnableGlobalMethodSecurity annotation to the class that inherits WebSecurityConfigurerAdapter to determine whether the user has access to a control layer method.

Control the interface permissions of the controller layer through the @PreAuthorize tag

public class SysRoleController {
    
    

    @Autowired
    private SysRoleService sysRoleService;

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @ApiOperation(value = "获取分页列表")
    @GetMapping("{page}/{limit}")
    public Result index(
            @ApiParam(name = "page", value = "当前页码", required = true)
            @PathVariable Long page,

            @ApiParam(name = "limit", value = "每页记录数", required = true)
            @PathVariable Long limit,

            @ApiParam(name = "roleQueryVo", value = "查询对象", required = false)
                    SysRoleQueryVo roleQueryVo) {
    
    
        Page<SysRole> pageParam = new Page<>(page, limit);
        IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo);
        return Result.ok(pageModel);
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @ApiOperation(value = "获取")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
    
    
        SysRole role = sysRoleService.getById(id);
        return Result.ok(role);
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.add')")
    @ApiOperation(value = "新增角色")
    @PostMapping("save")
    public Result save(@RequestBody @Validated SysRole role) {
    
    
        sysRoleService.save(role);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.update')")
    @ApiOperation(value = "修改角色")
    @PutMapping("update")
    public Result updateById(@RequestBody SysRole role) {
    
    
        sysRoleService.updateById(role);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")
    @ApiOperation(value = "删除角色")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
    
    
        sysRoleService.removeById(id);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")
    @ApiOperation(value = "根据id列表删除")
    @DeleteMapping("batchRemove")
    public Result batchRemove(@RequestBody List<Long> idList) {
    
    
        sysRoleService.removeByIds(idList);
        return Result.ok();
    }
    ...
}

3.8. Test server-side permissions

Log in to the background and assign permissions for testing. If the page has button permission control, it can be temporarily removed to facilitate testing.

Test conclusion:

1. Those assigned permissions can successfully return interface data

2. An exception will be thrown if there is no assigned permission: org.springframework.security.access.AccessDeniedException: Access is not allowed

3.9, exception handling

There are 2 ways to handle exceptions:

1. Extend Spring Security exception handling classes: AccessDeniedHandler, AuthenticationEntryPoint

2. Unified processing of global exceptions in spring boot

Description of the first solution: If the system implements global exception handling, then the global exception will first get the AccessDeniedException exception. If the Spring Security extension exception takes effect, the exception must be thrown again in the global exception.

We use the second option.

Add global exception handling

Operation module: service-util

/**
 * spring security异常
 * @param e
 * @return
 */
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException e) throws AccessDeniedException {
    
    
    return Result.build(null, ResultCodeEnum.PERMISSION);
}

AccessDeniedException needs to introduce dependencies, the corresponding exception of Spring Security

Introduce dependencies in the service-util module

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <scope>provided</scope>
</dependency>

Guess you like

Origin blog.csdn.net/AN_NI_112/article/details/131452010