day75(Spring Security(用户验证与授权),Bcrypt算法(加密工具):Spring Security的认证机制,JWT(JSON Web Token))

day75(Spring Security(用户验证与授权),Bcrypt算法(加密工具):Spring Security的认证机制,JWT(JSON Web Token))

1.Spring Security(用户验证与授权)

1.创建子模块(csmall-passport)

2.添加依赖:

<?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>cn.tedu</groupId>
        <artifactId>csmall-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <!-- 当前项目的信息 -->
    <groupId>cn.tedu</groupId>
    <artifactId>csmall-passport</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- 当前项目需要使用的依赖项 -->
    <dependencies>
        <!-- Spring Boot Web:支持Spring MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Boot Security:处理认证与授权 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring Boot Test:测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

3.启动项目

可以看到类似以下内容

Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92

Spring Security有默认登录的账号和密码(以上提示的值),密码是随机的,每次启动项目都会不同。

Spring Security默认要求所有的请求都是必须先登录才允许的访问,可以使用默认的用户名user和自动生成的随机密码来登录。在测试登录时,在浏览器访问当前主机的任意网址都可以(包括不存在的资源),会自动跳转到登录页(是由Spring Security提供的,默认的URL是:http://localhost:8080/login),当登录成功后,会自动跳转到此前访问的URL(跳转登录页之前的URL),另外,还可以通过 http://localhost:8080/logout 退出登录。

2.Bcrypt算法(加密工具)

Spring Security的依赖项中包括了Bcrypt算法的工具类,Bcrypt是一款非常优秀的密码加密工具,适用于对需要存储下来的密码进行加密处理。

1.应用测试

package cn.tedu.csmall.passport;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BcryptPasswordEncoderTests {

    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Test
    public void testEncode() {
        // 原文相同的情况,每次加密得到的密文都不同
        for (int i = 0; i < 10; i++) {
            String rawPassword = "123456";
            String encodedPassword = passwordEncoder.encode(rawPassword);
            System.out.println("rawPassword = " + rawPassword);
            System.out.println("encodedPassword = " + encodedPassword);
        }
        // rawPassword = 123456
        // encodedPassword = $2a$10$HWuJ9WgPazrwg9.isaae4u7XdP7ohH7LetDwdlTWuPC4ZAvG.Uc7W
        // encodedPassword = $2a$10$rOwgZMpDvZ3Kn7CxHWiEbeC6bQMGtfX.VYc9DCzx9BxkWymX6FbrS
        // encodedPassword = $2a$10$H8ehVGsZx89lSVHwBVI37OkxWm8LXei4T1o5of82Hwc1rD0Yauhky
        // encodedPassword = $2a$10$meBbCiHZBcYn7zMrZ4fPd.hizrsiZhAu8tmDk.P8QJcCzSQGhXSvq
        // encodedPassword = $2a$10$bIRyvV29aoeJLo6hh1M.yOvKoOud5kC7AXDMSUW4tF/DlcG0bLj9C
        // encodedPassword = $2a$10$eq5BuoAiQ6Uo0.TOPZOFPuRNlPl3t2GoTlaFoYfBu3/Bo3tLzx.v2
        // encodedPassword = $2a$10$DhTSwQfNdqrGgHRmILmNLeV0jt3ZXL435xz0fwyZ315ciI5AuI5gi
        // encodedPassword = $2a$10$T.8/ISoLOdreEEkp4py36O0ZYfihDbdHDuIElZVF3uEgMOX.8sPcK
        // encodedPassword = $2a$10$hI4wweFOGJ7FMduSmcjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW
        // encodedPassword = $2a$10$b4ztMI6tWoiJuoDYKwr7DOywsPkkCdvDxbPfmEsLdp11NdABS7wyy
    }

    @Test
    public void testMatches() {
        String rawPassword = "123456";
        String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW";
        boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
        System.out.println("match result : " + matchResult);
    }

}

2.使得Spring Security能使用数据信息验证用户身份

实现“根据用户名查询此用户的登录信息(应该包括权限信息)”的查询功能

SQL语句

select
    ams_admin.id,
    ams_admin.username,
    ams_admin.password,
    ams_admin.is_enable,
    ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id = ams_permission.id
where username='root';

1.在当前模块实现查询功能

1.添加依赖

  • mysql-connector-java
  • mybatis-spring-boot-starter
  • durid / druid-spring-boot-starter
<?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>cn.tedu</groupId>
        <artifactId>csmall-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <!-- 当前项目的信息 -->
    <groupId>cn.tedu</groupId>
    <artifactId>csmall-passport</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- 当前项目需要使用的依赖项 -->
    <dependencies>
        <!-- Csmall POJO -->
        <dependency>
            <groupId>cn.tedu</groupId>
            <artifactId>csmall-pojo</artifactId>
        </dependency>
        <!-- Spring Boot Web:支持Spring MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Boot Security:处理认证与授权 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Mybatis Spring Boot:Mybatis及对Spring Boot的支持 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!-- Spring Boot Test:测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

2.添加连接数据库的配置信息

application.yml

# 激活Profile配置
spring:
  # Profile配置
  profiles:
    # 激活Profile
    active: dev
  #连接数据库
  datasource:
    # 数据库连接
    type: com.alibaba.druid.pool.DruidDataSource

# Mybatis的XML文件位置
mybatis:
  # SQL的XML
  mapper-locations: classpath:mapper/*.xml

application-dev.yml

# 连接数据库的配置信息
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    # 日志的显示级别
logging:
  level:
    cn.tedu.csmall: trace

3.创建MybatisConfiguration配置类,用于配置@MapperScan

package cn.tedu.csmall.passport.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("cn.tedu.csmall.passport.mapper")
public class MybatisConfiguration {

}

4.在配置文件中配置mybatis.mapper-locations属性,以指定XML文件的位置(第2步已设置)

5.创建AdminLoginVO

@Data
public class AdminLoginVO implements Serializable {
    private Long id;
    private String username;
    private String password;
    private Integer isEnable;
    private List<String> permissions;
}

6.在src/main/java下的cn.tedu.csmall.passport包下创建mapper.AdminMapper.java接口并添加抽象方法

package cn.tedu.csmall.passport.mapper;

import cn.tedu.csmall.pojo.vo.AdminLoginVO;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminMapper {
    /**
     * 根据用户名查询管理员的登录信息
     * @param username 用户名
     * @return 匹配的管理员的登录信息,如果没有匹配的数据,则返回null
     */
    AdminLoginVO getLoginInfoByUsername(String username);
}

7.在src/main/resources下创建mapper文件夹,并在此文件夹下粘贴得到AdminMapper.xml

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

<mapper namespace="cn.tedu.csmall.passport.mapper.AdminMapper">

    <!-- AdminLoginVO getLoginInfoByUsername(String username); -->
    <select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
        select
        <include refid="LoginInfoQueryFields" />
        from ams_admin
        left join ams_admin_role
        on ams_admin.id = ams_admin_role.admin_id
        left join ams_role_permission
        on ams_admin_role.role_id = ams_role_permission.role_id
        left join ams_permission
        on ams_role_permission.permission_id = ams_permission.id
        where username=#{username}
    </select>

    <sql id="LoginInfoQueryFields">
        <if test="true">
            ams_admin.id,
            ams_admin.username,
            ams_admin.password,
            ams_admin.is_enable,
            ams_permission.value
        </if>
    </sql>

    <resultMap id="LoginInfoResultMap" type="cn.tedu.csmall.pojo.vo.AdminLoginVO">
        <id column="id" property="id" />
        <result column="username" property="username" />
        <result column="password" property="password" />
        <result column="is_enable" property="isEnable" />
        <collection property="permissions" ofType="java.lang.String">
            <!-- 以下配置类似在Java中执行 new String("/pms/product/read") -->
            <constructor>
                <arg column="value" />
            </constructor>
        </collection>
    </resultMap>

</mapper>

8.创建脚本

truncate.sql

truncate ams_admin;
truncate ams_admin_role;
truncate ams_role;
truncate ams_role_permission;
truncate ams_permission;

insert_data.sql

-- 注意:管理员的测试数据中,插入的密码值应该是密文值
-- 以下插入的管理员数据的密码原文均是:123456

-- 管理员表:插入测试数据
insert into ams_admin (username, password, nickname, email, description, is_enable) values
('root', '$2a$10$H8ehVGsZx89lSVHwBVI37OkxWm8LXei4T1o5of82Hwc1rD0Yauhky', 'root', 'root@tedu.cn', '最高管理员', 1),
('super_admin', '$2a$10$H8ehVGsZx89lSVHwBVI37OkxWm8LXei4T1o5of82Hwc1rD0Yauhky', 'administrator', 'admin@tedu.cn', '超级管理员', 1),
('nobody', '$2a$10$H8ehVGsZx89lSVHwBVI37OkxWm8LXei4T1o5of82Hwc1rD0Yauhky', '无名', 'liucs@tedu.cn', null, 0);

-- 权限表:插入测试数据
insert into ams_permission (name, value, description) values
('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),
('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),
('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),
('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),
('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),
('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');

-- 角色表:插入测试数据
insert into ams_role (name) values
('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');

-- 角色权限关联表:插入测试数据
insert into ams_role_permission (role_id, permission_id) values
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),
(2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6),
(3, 1), (3, 2), (3, 3);

-- 管理员角色关联表:插入测试数据
insert into ams_admin_role (admin_id, role_id) values
(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);

9.测试

package cn.tedu.csmall.passport.mapper;

import cn.tedu.csmall.pojo.vo.AdminLoginVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;

@SpringBootTest
public class AdminMapperTests {

    @Autowired
    AdminMapper mapper;

    @Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
    @Test
    void testGetLoginInfoByUsernameSuccessfully() {
        // 测试数据
        String username = "root";
        // 断言不会抛出异常
        Assertions.assertDoesNotThrow(() -> {
            // 执行查询
            AdminLoginVO admin = mapper.getLoginInfoByUsername(username);
            System.out.println("result >>> " + admin);
            // 断言查询结果不为null
            Assertions.assertNotNull(admin);
        });
    }

    @Sql({"classpath:truncate.sql"})
    @Test
    void testGetLoginInfoByUsernameFailBecauseNotFound() {
        // 测试数据
        String username = "root";
        // 断言不会抛出异常
        Assertions.assertDoesNotThrow(() -> {
            // 执行查询
            AdminLoginVO admin = mapper.getLoginInfoByUsername(username);
            System.out.println("result >>> " + admin);
            // 断言查询结果为null
            Assertions.assertNull(admin);
        });
    }

}

3.改进

根据上述方法查询到结果例如:

AdminLoginVO(
	id=1, 
	username=root, 
	password=1234, 
	isEnable=1, 
	permissions=[
		/pms/product/read, 
		/pms/product/update, 
		/pms/product/delete, 
		/ams/admin/read, 
		/ams/admin/update, 
		/ams/admin/delete
	]
)

1.Spring Security的认证机制

  1. 当客户端提交登陆后,会自动调用UserDetailsService接口(Spring Security定义的)的实现类对象中的UserDetails的loadUserByUsername(String username)方法(根据用户名加载用户数据)

  2. 得到UserDetails类型的对象,此对象中应该至少包括此用户名对应的密码、权限等信息

  3. Spring Security会自动完成密码的对比,并确定此次客户端提交的信息是否允许登录,类似于

    // Spring Security的行为
    UserDetails userDetails = userDetailsService.loadUserByUsername("chengheng");
    // Spring Security将从userDetails中获取密码,用于验证客户端提交的密码,判断是否匹配
    
  4. 所以,要实现Spring Security通过数据库的数据来验证用户名与密码(而不是采用默认的user用户名和随机的密码),则在cn.tedu.csmall.passport包下创建security.UserDetailsServiceImpl类,实现UserDetailsService接口,并重写接口中的抽象方法

    package cn.tedu.csmall.passport.security;
    
    import cn.tedu.csmall.passport.mapper.AdminMapper;
    import cn.tedu.csmall.pojo.vo.AdminLoginVO;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.core.userdetails.User;
    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.Service;
    
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private AdminMapper adminMapper;
    
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            System.out.println("根据用户名查询尝试登录的管理员信息,用户名=" + s);
            AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
            System.out.println("通过持久层进行查询,结果=" + admin);
    
            if (admin == null) {
                System.out.println("根据用户名没有查询到有效的管理员数据,将抛出异常");
                throw new BadCredentialsException("登录失败,用户名不存在!");
            }
    
            System.out.println("查询到匹配的管理员数据,需要将此数据转换为UserDetails并返回");
            UserDetails userDetails = User.builder()
                    .username(admin.getUsername())
                    .password(admin.getPassword())
                    .accountExpired(false)
                    .accountLocked(false)
                    .disabled(admin.getIsEnable() != 1)
                    .credentialsExpired(false)
                    .authorities(admin.getPermissions().toArray(new String[] {}))
                    .build();
            System.out.println("转换得到UserDetails=" + userDetails);
            return userDetails;
        }
    
    }
    
  5. 再配置密码加密器即可:

    package cn.tedu.csmall.passport.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfiguration {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    }
    
  6. 重启项目,在浏览器上访问此项目的任何URL,进入登录页,即可使用数据库中的管理员数据进行登录。

2.JWT(JSON Web Token)

1.Session(简单项目)

在Spring Security,默认使用Session机制存储成功登录的用户信息(因为HTTP协议是无状态协议,并不保存客户端的任何信息,所以,同一个客户端的多次访问,对于服务器而言,等效于多个不同的客户端各访问一次,为了保存用户信息,使得服务器端能够识别客户端的身份,必须采取某种机制),当下,更推荐使用Token或相关技术(例如JWT)来解决识别用户身份的问题。

2.JWT

1.概念:

JWT = JSON Web Token,它是通过JSON格式组织必要的数据,将数据记录在票据(Token)上,并且,结合一定的算法,使得这些数据会被加密,然后在网络上传输,服务器端收到此数据后,会先对此数据进行解密,从而得到票据上记录的数据(JSON数据),从而识别用户的身份,或者处理相关的数据。

2.组成
1.Header(头):指定算法和当前数据类型
2.Payload(载荷):Claims(自定义数据)和过期时间
3.Signature(签名):算法和密钥
3.运行流程:

其实,在客户端第1次访问服务器端时,是“空着手”访问的,不会携带任何票据数据,当服务器进行响应时,会将JWT响应到客户端,客户端从第2次访问开始,每次都应该携带JWT发起请求,则服务器都会收到请求中的JWT并进行处理。

4.依赖:(jjwt)
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

则在根项目中管理以上依赖,并在csmall-passport中添加以上依赖。

5.测试使用JWT
package cn.tedu.csmall.passport;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtTests {

    // 密钥
    String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";

    @Test
    public void testGenerateJwt() {
        // Claims
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("name", "星星");

        // JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
        String jwt = Jwts.builder()
                // Header:指定算法与当前数据类型
                // 格式为: { "alg": 算法, "typ": "jwt" }
                .setHeaderParam(Header.CONTENT_TYPE, "HS256")
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                // Payload:通常包含Claims(自定义数据)和过期时间
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                // Signature:由算法和密钥(secret key)这2部分组成
                .signWith(SignatureAlgorithm.HS256, secretKey)
                // 打包生成
                .compact();

        // eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NTM2NTY3N30.QwBYVgdkdibEpD-pjX4sKfNu3tw8hBLcJy4-UcN1F3c
        // eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NTM2NzMwMn0.qBBHearv8iHPNjtDGtO2ci_-KAL4CALHnwzaG_ljsQg
        System.out.println(jwt);
    }

    @Test
    public void testParseJwt() {
        String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoi5pif5pifIiwiaWQiOjk1MjcsImV4cCI6MTY1NTM2NzMwMn0.qBBHearv8iHPNjtDGtO2ci_-KAL4CALHnwzaG_ljsQg";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Object id = claims.get("id");
        Object name = claims.get("name");
        System.out.println("id=" + id);
        System.out.println("name=" + name);
    }

}
6.异常信息
1.当JWT数据过期时,异常信息例如:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-06-16T15:47:57Z. Current time: 2022-06-16T16:08:32Z, a difference of 1235869 milliseconds.  Allowed clock skew: 0 milliseconds.
2.当JWT解析失败(数据有误)时,异常信息例如:
io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"cty"�"HS256","typ":"JWT","alg":"HS256"}
3.当生成JWT和解析JWT的密钥不一致时,异常信息例如:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

3.使用JWT(在Spring Security中)

要求:

1.不能让Spring Security按照原有模式来处理登录,需要自定义处理登录过程(原有模式中,登录成功后,自动装用户信息存储到Session中,且跳转页面)

2.过程:

1.需要自动装配AuthenticationManager(身份验证管理器)对象
  • 使得SecurityConfiguration配置类继承自WebSecurityConfigurerAdapter类,重写其中的xx方法,在此方法中直接调用父级方法即可,并在此方法上添加@Bean注解

    package cn.tedu.csmall.passport.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    //直接调用父类的方法返回需要的AuthenticationManager对象
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 禁用防跨域攻击
            http.csrf().disable();
    
            // URL白名单
            String[] urls = {
                "/admins/login"
            };
    
            // 配置各请求路径的认证与授权
            http.authorizeRequests() // 请求需要授权才可以访问
                .antMatchers(urls) // 匹配一些路径
                .permitAll() // 允许直接访问(不需要经过认证和授权)
                .anyRequest() // 匹配除了以上配置的其它请求
                .authenticated(); // 都需要认证
        }
    }
    
2.创建AdminLoginDTO类,此类中应该包含用户登录时需要提交的用户名、密码
package cn.tedu.csmall.pojo.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class AdminLoginDTO implements Serializable {

    private String username;
    private String password;

}
3.创建IAdminService接口
4.在IAdminService接口中添加登录的抽象方法
package cn.tedu.csmall.passport.service;

import cn.tedu.csmall.pojo.dto.AdminLoginDTO;

public interface IAdminService {

    String login(AdminLoginDTO adminLoginDTO);

}
5.创建AdminServiceImpl类,实现以上接口
  • 在实现过程中,调用AuthenticationManager实现认证,当认证成功后,生成JWT并返回

    package cn.tedu.csmall.passport.service;
    
    import cn.tedu.csmall.pojo.dto.AdminLoginDTO;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.stereotype.Service;
    
    @Service
    public class AdminServiceImpl implements IAdminService {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Override
        public String login(AdminLoginDTO adminLoginDTO) {
            // 准备被认证数据
            Authentication authentication
                    = new UsernamePasswordAuthenticationToken(
                            adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
            // 调用AuthenticationManager验证用户名与密码
            // 执行认证,如果此过程没有抛出异常,则表示认证通过,如果认证信息有误,将抛出异常
             Authentication authenticate = authenticationManager.authenticate(authentication);
    
            // 如果程序可以执行到此处,则表示登录成功
            // 生成此用户数据的JWT
            String jwt = "This is a JWT."; // 临时
            return jwt;
        }
    
    }
    
6.创建AdminController类,在类中处理登录请求
package cn.tedu.csmall.passport.controller;

import cn.tedu.csmall.passport.service.IAdminService;
import cn.tedu.csmall.pojo.dto.AdminLoginDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {

    @Autowired
    private IAdminService adminService;

    // http://localhost:8080/admins/login?username=root&password=123456
    @RequestMapping("/login")
    public String login(AdminLoginDTO adminLoginDTO) {
        String jwt = adminService.login(adminLoginDTO);
        return jwt;
    }

}
7.在SecurityConfiguration中配置Spring Security,对特定的请求进行放行(默认所有请求都必须先登录)
<===============原有代码==================>

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用防跨域攻击
        http.csrf().disable();

        // URL白名单
        String[] urls = {
            "/admins/login"
        };

        // 配置各请求路径的认证与授权
        http.authorizeRequests() // 请求需要授权才可以访问
            .antMatchers(urls) // 匹配一些路径
            .permitAll() // 允许直接访问(不需要经过认证和授权)
            .anyRequest() // 匹配除了以上配置的其它请求
            .authenticated(); // 都需要认证
    }
}
8.测试

以上全部完成后,启动项目,打开浏览器,可以通过 http://localhost:8080/admins/login?username=root&password=123456 这类URL测试登录,使用数据库中的用户名和密码进行尝试。

3.当通过以上URL进行访问时,其内部过程大概是:

1.Spring Security的相关配置会进行URL的检查,来判断是否允许访问此路径
  • 所以,需要在SecurityConfiguration中将以上路径设置为白名单
  • 如果没有将以上路径配置到白名单,将直接跳转到登录页,因为默认所有请求都必须先登录
2.由AdminController接收到请求后,调用了IAdminService接口的实现类对象来处理登录
  • IAdminService接口的实现是AdminServiceImpl
3.在AdminServiceImpl中,调用了AuthenticationManager处理登录的认证
  • AuthenticationManager对象调用authenticate()方法进行登录处理
    • 内部实现中,会自动调用UserDetailsService实现对象的loadUserByUsername()方法以获取用户信息,并自动完成后续的认证处理(例如验证密码是否正确)
    • 所以,在步骤中,具体执行的是UserDetailsServiceImpl类中重写的方法,此方法返回了用户信息,Spring Security自动验证,如果失败(例如账号已禁用、密码错误等),会抛出异常
  • 以上调用的authenticate()方法如果未抛出异常,可视为认证成功,即登录成功
  • 当登录成功时,应该返回此用户的JWT数据(暂时未实现)
posted @ 2022-06-16 21:35  约拿小叶  阅读(420)  评论(0编辑  收藏  举报