前语

登录这东西很古怪哎,你说它难吗?好像客户端只需求调接口就行,那有啥难的?当你多多少少对登录的后台有些了解,又觉得好难啊,session,token,cookie,等等一堆东西,有老的咱们都不喜爱用的,有新的一些不太懂的,根据公司项目规模不同,还要考虑本钱的问题,真是有些头疼。博主今天引荐的一种登陆办法便是Spring Security + JWT的结合运用,为什么要两者结合呢?Spring Security现在现已很少用了,乃至有些人以为现已废弃了,可是由于Spring Security是Spring系列的东西,Spring对其支撑很友爱,不,是十分友爱。可是咱们不想运用他验证后的操作,所以咱们要打断这个操作,让JWT作业。下面咱们就来了解并手动操作一下吧,本篇仍是集成在咱们前面的微服务项目中,你也能够另起项目一起来做。

为什么要登录

咱们平常都知道登录,不知道有没有思考过登录处理的是什么问题?咱们会想到,不登录就不能拿到用户信息,一些用户行为和服务就没有办法相关到具体人身上,没错!比方购买行为。但我觉得这个说法不可具体,登录的具体作用应该是拿到用户的权限。

咱们说,是人,就有不同的人物,一个男人,能够是儿子,能够是父亲,能够是职工,能够是老板等等。那咱们就以为,一个登录系统中,有必要要有一张用户表和一张人物表,还有刚刚说的权限表。这三张表之间还需求有标明其联系的相关表。

能够说,这三张表在任何一个登录系统都是必备的,乃至你还能够有暂时的用户权限表,他们之间多是多对多的联系,要理清他们之间的联系并不简单。

登录的品种

登录的品种到目前为止所运用的技能大约五六种吧,其之间迥然不同,从早期的Cookie-Session到现在的单点登录,中心跨过的时刻不短,其间有一个时刻分割点便是html5规范呈现的时候,他带来了local storage,使得跨域问题得到良好的处理,但咱们并不满足于这种办法,所以token技能呈现,可是本质上和基于Cookie-Session+local storage的办法没有太大区别。为了处理微服务间的数据同步,基于Token的JWT认证诞生,其间还有一种利用session和redis的数据同享技能也能完成数据同享,这和token技能也相似。接下来,咱们来简单的了解一下这几种登录办法:

Cookie-Session

这种办法要追溯到html5呈现之前,那时只能利用cookie存储SessionId,但cookie在跨域问题上一言难尽,但并不是不能跨域,仅仅要比咱们后边的办法麻烦,有local storage你还用cookie?而且cookie退出站点后就会销毁,这点让人极不能承受。其流程是:

  • 用户输入用户名、暗码或短信验证码登录
  • 服务端验证后,回来一个 SessionId,其和用户信息相关。客户端将 SessionID存到cookie
  • 当客户端再建议恳求时带上cookie中的信息,服务端经过cookie获取 SessionID并校验,以判断用户是否登录

Cookie-Session-local storage

这种办法和以上相似,仅仅改了几个当地:

  • 存储的位置不再是cookie,而是local storage
  • 服务端不存储sessionid,而是改用redis做存储,能够处理同步问题,但也有缺点,同步会造成数据量增加,占用额定内存,咱们经过一张图来阐明

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

左面先行,获取用户信息,生成sessionid,存储在redis,右边拜访其他模块,经过sessionid去redis拿用户信息,留意,用户模块和其他模块也会保存sessionid,这便是数据同享,用户量很大的状况会造成数据冗余,不适合用户量特别大的项目,中小型项目能够。对于客户端,sessionid当然是保存在local storage内了,毕竟谁也不想去额定处理跨域的问题。

JWT令牌

这种办法是目前运用比较多的一种办法,它和上面的办法也有相似之处,仅仅少了数据的存储,JWT不存储session这些东西,它只担任验证jwt是否正确,验证的过程便是解码的过程,关于JWT的规范制式的解说,请咱们手动百度吧,不再赘述,博主也记不住,贴了浪费篇幅。看看,大约知道是怎样做的就行。

此处有必要有图:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

服务端不保存信息,这一点能够节约空间,谁的信息谁自己保存,解密办法在我这儿,一起提高了安全性,何乐不为?

几种登陆总结

假如细分还能再分出几种登录办法,但根本迥然不同,博主兼并了其间相似的登录办法,总结出来这三种,此处疏忽第三方登录,可自行了解。肯定还有其他办法,但总的来说,和这三种应该是相似,并不会彻底不同。看了一篇OAuth2.0单点登录相关的文章,还有一篇总结登录的文章,真是写的太好了,分享给咱们:

安全验证 – 知乎

Java——项目常用登录办法详解_new 海绵宝宝()的博客-CSDN博客

里面总结的很全面,也有一些案例,初学者能够看看。

用户身份认证与授权

从这儿开端,便是咱们的项目时刻,首要进场的是Spring Security,它是用于处理认证与授权的结构。Spring Security有默许的登录账号和暗码,用户名user,暗码是随机的,每次发动项目都会从头生成一个。它要求一切的恳求都有必要先登录才答应拜访,稍后咱们集成后能够来进行测验。

创立工程

在微服务项目cloud下创立cloud-passport子项目:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

增加依靠

<?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.codingfire</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.codingfire</groupId>
    <artifactId>cloud-passport</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud-passport</name>
    <description>Demo project for Spring Boot</description>
    <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>

父子相关

<modules>
    <module>cloud-commons</module>
    <module>cloud-bussiness</module>
    <module>cloud-cart</module>
    <module>cloud-order</module>
    <module>cloud-stock</module>
    <module>gateway</module>
    <module>search</module>
    <module>cloud-passport</module>
</modules>

发动项目

依靠增加完毕,什么都不需求做,直接运转passport的发动文件,能够在操控台看到如下输出:

Using generated security password: 1060ee9f-a56e-4ff5-bce4-68306b3265b1

这便是Spring Security生成的随机暗码,它一起还提供了一个URL:http://localhost:8080/login

咱们点开URL,在浏览器打开一个登录页面,咱们输入用户名:user,暗码就用上面的暗码,登录成功后跳转回之前拜访的URL,由于咱们没有做这个页面,会显现404。这便是Spring Security默许要求一切的恳求都是有必要先登录才答应的拜访的能力。

Bcrypt算法的东西

Spring Security的依靠项中包含了Bcrypt算法的东西类,这是一款十分优异的暗码加密东西,适和对需求存储下来的暗码进行加密处理。咱们来测验下看看。

打开测验类,增加如下测验代码:

package com.codingfire.cloud.passport;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class CloudPassportApplicationTests {
    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);
        }
    }
    @Test
    public void testMatches() {
        String rawPassword = "123456";
        String encodedPassword = "$2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5K";
        boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword);
        System.out.println("match result : " + matchResult);
    }
}

咱们分别运转这两个办法,会看到如下输出:

rawPassword = 123456
encodedPassword = $2a$10$4LHozWwptKuabvikrzM1KefYFgI7H4A9xCVv7cvMKsV9ycS4guS5K
        rawPassword = 123456
encodedPassword = $2a$10$VA9u7X9rSvuEtPlEixhnSujHdVsK8OwqkVIOqLzNydxa.ypCviVIq
        rawPassword = 123456
encodedPassword = $2a$10$d9lWItH5YhEFRns/Yj5U3OUyHM8rLKAE9X.SsbcIOA0WwRqUwFl82
        rawPassword = 123456
encodedPassword = $2a$10$W/PLc/Q04.8xfmEQgwSKC.g79FxRPJGFXRuFzISdVrn3cYWk1xkye
        rawPassword = 123456
encodedPassword = $2a$10$/9Ya1aqjQBX8342iH5blTOZeHJomKUitInVmLTsANonXriQjxhb5K
        rawPassword = 123456
encodedPassword = $2a$10$kX2u5zLrDN/VC8CLVRGmsOIFqA2FHCJRYJKnYmWeu/NyTQEjBCbki
        rawPassword = 123456
encodedPassword = $2a$10$igB96QfY9XDwhPz3U8Z7Nui1UQy.wtzSl9uk2n7m.lCdcKwhGqLXu
        rawPassword = 123456
encodedPassword = $2a$10$ssDypFmm0bN0CvIBqoB4huHIhT7oRwS9KsO1iopyFeSOUWYR96NPC
        rawPassword = 123456
encodedPassword = $2a$10$IWBuDVLYjvHCUqOM9qAQuu.kTlW8RH08CbIFlvYTzcdEMLHbVSFtS
        rawPassword = 123456
encodedPassword = $2a$10$J/eN5/loO6DTJG7ubgQh4.1ovwI9CS1H0yqnsbYEQFwnvqRq64bU.
match result : true

下面的解密运用上面的第一个加密后的密文进行的解密。咱们要用自己的电脑生成的密文进行解密,用博主的或许会呈现无法匹配的状况。

此加密东西有个特点,咱们应该发现了,此加密得到的密文都不相同。

接着需求和数据库中存储的密文进行比照,此刻需求运用SQL去数据库查询该用户的密文进行比对,比对经过,则可进行登录。此刻就不能运用默许的user用户名和随机的暗码的办法,具体做法咱们持续往下看。

创立VO模型类

在commons工程下创立pojo.passport.vo.AdminLoginVO类:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

package com.codingfire.cloud.commons.pojo.passport.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class AdminLoginVO implements Serializable {
    private Long id;
    private String username;
    private String password;
    private Integer isLogin;
    private List<String> permissions;
}

创立完成后咱们发现要运用commons模块,那需求依靠增加此模块:

<!--all-common依靠-->
<dependency>
    <groupId>com.codingfire</groupId>
    <artifactId>cloud-commons</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

创立接口文件

在passport下创立mapper.AdminMapper接口:

package com.codingfire.cloud.passport.mapper;
import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;
public interface AdminMapper {
    AdminLoginVO getLoginInfoByUsername(String username);
}

创立XML文件

咱们还记得吗?咱们在Mybatis结构中有运用XML文件来写SQL。在src/main/resources下创立mapper文件夹,mapper文件夹下能够把前面的xml文件粘贴过来,写入如下SQL:

<?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="com.codingfire.cloud.passport.mapper.AdminMapper">
    <!-- AdminLoginVO getLoginInfoByUsername(String username); -->
    <select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
        select
        <include refid="LoginInfoQueryFields" />
        from admin
        left join admin_role
        on admin.id = admin_role.admin_id
        left join role_permission
        on admin_role.role_id = role_permission.role_id
        left join permission
        on role_permission.permission_id = permission.id
        where username=#{username}
    </select>
    <sql id="LoginInfoQueryFields">
        <if test="true">
            admin.id,
            admin.username,
            admin.password,
            admin.is_login,
            permission.name
        </if>
    </sql>
    <resultMap id="LoginInfoResultMap" type="com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO">
        <id column="id" property="id" />
        <result column="username" property="username" />
        <result column="password" property="password" />
        <result column="is_login" property="isLogin" />
        <collection property="permissions" ofType="java.lang.String">
            <constructor>
                <arg column="name" />
            </constructor>
        </collection>
    </resultMap>
</mapper>

在这儿咱们要留意几个问题了,咱们这儿需求衔接mybatis的数据库,第一次看博主文章的需求看看Java开发 – Mybatis结构初体验_CodingFire的博客-CSDN博客

这篇博客,才知道建的什么数据库, 有哪些表,有哪些参数,否则将很难进行下去。

弥补装备

由于需求运用数据库,需求弥补装备和依靠。

增加依靠

<!--mybatis整合springboot-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--alibaba 数据源德鲁伊-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

增加装备

这儿,咱们挑选从mybatis复制装备信息到properties文件:

spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.driver=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=0
mybatis.mapper-locations=classpath:mapper/AdminMapper.xml

暗码写自己的数据库暗码。

创立装备类

需求衔接数据库,那么少不了mybatis装备了,创立MybatisConfiguration类,在passport下创立config包,此包下创立装备类:

package com.codingfire.cloud.passport.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.codingfire.cloud.passport.mapper")
public class MybatisConfiguration {
}

前面也是有创立过的,你能够挑选直接贴过来,但要留意扫描的途径改成自己的途径。原本需求在装备文件中装备mybatis.mapper-locations特点,上面现已弥补过了。

测验上面的装备

在测验类下,咱们增加如下代码:

    @Autowired
    AdminMapper adminMapper;
    @Test
    void selectUser() {
        AdminLoginVO adminLoginVO = adminMapper.getLoginInfoByUsername("admin04");
        System.out.println(adminLoginVO);
    }

这是咱们本来表中的数据,没有数据的需求预先插入一些数据。运转测验办法,发现报错?

额,一大堆,看了……好一会儿,才发现有两个当地写错了:

一个是AdminMapper内没有增加@Repository注解:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

另一个是MybatisConfiguration类上的scan注解写错了,修正一下:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

然后再次运转测验办法,能够在操控台看到输出的用户信息如下:

AdminLoginVO(id=1, username=admin04, password=123456, isLogin=0, permissions=[全频道可删去, 全频道可挑选, 全频道读取, 单频道可删去, 单频道可挑选, 单频道观看])

代表咱们的测验成功了。几乎累的一逼,真是错一步都不可。

让Spring Security经过数据库验证暗码

前面提过,要让Spring Security经过数据库的数据来验证用户名与暗码,咱们还需求做出一些修正和装备,咱们看到每次操控台都会输出一串新的暗码:

Using generated security password: a47b9983-3ea3-45d8-9632-faf701a7925b

下面,让咱们看看怎样才干不让它输出。

装备暗码加密器

在config包下创立SecurityConfiguration类:

package com.codingfire.cloud.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();
    }
}

重写Spring Security下的用户相关笼统办法

在passport下建新包security,包下建类UserDetailsServiceImpl,并完成UserDetailsService接口:

package com.codingfire.cloud.passport.security;
import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;
import com.codingfire.cloud.passport.mapper.AdminMapper;
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;
@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.getIsLogin() != 1)
                .credentialsExpired(false)
                .authorities(admin.getPermissions().toArray(new String[] {}))
                .build();
        System.out.println("转化得到UserDetails=" + userDetails);
        return userDetails;
    }
}

测验成果

从头发动工程,看看还有没有随机暗码生成:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

能够看到,随机暗码现已不会再自动生成。

JWT

什么是JWT

Json web token (JWT) , 是为了在网络应用环境间传递声明而履行的一种基于JSON的敞开规范((RFC 7519).界说了一种简练的,自包含的办法用于通讯两边之间以JSON目标的形式安全的传递信息。 由于数字签名的存在,这些信息是可信的,JWT能够运用HMAC算法或者是RSA的公私秘钥对进行签名。

客户端第1次拜访服务器端时,是没有带着令牌拜访的,当服务器进行呼应时,会将JWT呼应到客户端,客户端保存后,在第2次拜访时就开端带着JWT进行恳求,服务器收到恳求中的JWT后就能够辨认用户身份。

关于JWT的具体介绍,引荐这篇博客:SpringBoot集成JWT完成token验证 – 简书

为什么运用JWT

Spring Security默许运用Session机制存储用户信息,而HTTP协议是无状况协议,它不保存客户端信息,所以,同一个客户端的屡次拜访,等效于多个不同的客户端各拜访一次服务端,为了保存用户信息,使服务器端能够辨认客户端身份,咱们引荐运用Token或其他技能,比方咱们马上要说的JWT。

如何运用JWT

增加依靠

JWT仅仅一个概念,而完成生成JWT、解析JWT的结构却有不少,咱们这儿要运用的是jjwt,增加依靠如下:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

由于版别现已在主项目中操控,此处版别省略。

测验jwt

在测验类下创立JwtTests类,增加如下测验代码:

// 密钥,遵从越长越好,越乱越杂乱越好的原则
    String secretKey = "asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";
    @Test
    public void testGenerateJwt() {
        // Claims
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 01);
        claims.put("name", "codingfire");
        // 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();
        System.out.println(jwt);
    }

运转测验办法,输出加密后的密文如下:

eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As

你能看到里面有两个点,这是JWT加密的固定格局,需求你去看引荐的博文。

接着咱们把这串密文用来解密试试看能得到什么:

    @Test
    public void testParseJwt() {
        String jwt = "eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As";
        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);
    }

运转测验办法:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

看到如图所示成果,你的jwt就现已引进成功。但,这还不可,咱们是要在Spring Security中运用JWT,所以还有许多作业要做。

在Spring Security中运用JWT

自动安装AuthenticationManager目标

这是一个认证管理器,咱们需求接收这个管理器,在SecurityConfiguration类中做一些操作,来看看终究的SecurityConfiguration类吧:

package com.codingfire.cloud.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();
    }
    @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(); // 都需求认证
    }
}

创立DTO类

在上面创立AdminLoginVO类的当地创立新的包dto,下面建新类:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

创立接口类

在passport下创立service包,其下创立新接口类IAdminService:

package com.codingfire.cloud.passport.service;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
public interface IAdminService {
    String login(AdminLoginDTO adminLoginDTO);
}

创立完成类

在service包下创立新包impl,其下创立完成类AdminServiceImpl:

package com.codingfire.cloud.passport.service.impl;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
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) {
        // 生成此用户数据的JWT
        String jwt = "This is a JWT."; // 暂时
        return jwt;
    }
}

创立操控器类

在passport下创立controller包,其下创立AdminController:

package com.codingfire.cloud.passport.controller;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
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;
    @RequestMapping("/login")
    public String login(AdminLoginDTO adminLoginDTO) {
        String jwt = adminService.login(adminLoginDTO);
        return jwt;
    }
}

测验代码

发动项目,在浏览器输入:http://localhost:8080/admins/login?username= codingfire&password=123456

把用户名和暗码改成你自己数据库中的用户名和暗码,也能够写错的,然后进行屡次测验,看浏览器会回来什么:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

看到此信息,就代表你的jwt接入成功了,但咱们需求回来给客户端jwt数据,接下来咱们完成这个过程。

回来客户端JWT数据

修正AdminServiceImpl完成类

package com.codingfire.cloud.passport.service.impl;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.passport.service.IAdminService;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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.security.core.userdetails.User;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class AdminServiceImpl implements IAdminService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Override
    public String login(AdminLoginDTO adminLoginDTO) {
        // 密钥,遵从越长越好,越乱越杂乱越好的原则
        String secretKey = "asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";
        // 预备被认证数据
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(
                adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
        // 调用AuthenticationManager验证用户名与暗码
        // 履行认证,假如此过程没有抛出反常,则表示认证经过,假如认证信息有误,将抛出反常
        authenticationManager.authenticate(authentication);
        User user = (User) authentication.getPrincipal();
        System.out.println("从认证成果中获取Principal=" + user.getClass().getName());
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", user.getUsername());
        claims.put("permissions", user.getAuthorities());
        System.out.println("行将向JWT中写入数据=" + claims);
        // 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();
        // 回来JWT数据
        return jwt;
    }
}

你会发现,这便是咱们在测验类中测验的代码,根本上是直接贴过来的。

修正操控器类

package com.codingfire.cloud.passport.controller;
import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.passport.service.IAdminService;
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;
    @RequestMapping("/login")
    public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
        String jwt = adminService.login(adminLoginDTO);
        return JsonResult.ok(jwt);
    }
}

修正回来值类型。

测验jwt数据回来

运转项目,在浏览器输入本来的html:http://localhost:8080/admins/login?username= codingfire&password=123456

浏览器将得到如下数据:

{"state":200,"message":"eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJwZXJtaXNzaW9ucyI6Ilt7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWFqOmikemBk-ivu-WPllwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-WIoOmZpFwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-WPr-etm-mAiVwifSx7XCJhdXRob3JpdHlcIjpcIuWNlemikemBk-ingueci1wifV0iLCJleHAiOjE2Nzc3NDkzOTUsInVzZXJuYW1lIjoiY29kaW5nZmlyZSJ9.dw4tk52xTXXQ4-D_qkZNhjL-RkHnzG6QKHe6Tq1j3_Y","data":null}

这儿有个坑啊小伙伴们,假如你一直403,且操控台提示你Encoded password does not look like BCrypt,这是由于你的数据库存储的是明文暗码,有必要存储咱们在测验类中运用BCryptPasswordEncoder加密后的暗码。博主刚刚就犯了这个错,实在太容易疏忽了,不知道该说啥,咱们可别犯这个错。

运用其他URL被屏蔽怎样办

刚刚由于咱们制止了未登陆时直接进入Spring Security的登陆页,所以才需求增加了白名单处理屏蔽一切衔接的问题。假如运用Knife4j,该怎样增加白名单呢?咱们来看看:

        String[] urls = {
                "/admins/login",
                "/doc.html",  // 从本行开端,以下是新增
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs",
                "/favicon.ico"
        };

运用恳求头

得到JWT之后,在后续的恳求中都需求在恳求头中带上JWT,放在Authorization特点内,所以应该先判断恳求头中是否有Authorization,而不能让恳求直达服务器事务模块。这让我想到了前面讲过的过滤器,下面,咱们在security包下创立一个过滤器类:

package com.codingfire.cloud.passport.security;
import org.springframework.stereotype.Component;
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;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthenticationFilter.doFilterInternal()");
    }
}

过滤器类是需求注册后才干作业的,所以下一步对过滤器进行注册。用于验证JWT的过滤器应该运转在Spring Security处理登录的过滤器之前才干作业,所以需求在自界说的SecurityConfiguration中的configure()办法中将咱们自界说的过滤器注册在Spring Security的相关过滤器之前。

同一个项目中答应存在多个过滤器,构成过滤器链,所以咱们注册过滤器不需求单独建个类来处理了,而是在SecurityConfiguration类中进行,终究的类如下:

package com.codingfire.cloud.passport.config;
import com.codingfire.cloud.passport.security.JwtAuthenticationFilter;
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.method.configuration.EnableGlobalMethodSecurity;
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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @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",
                "/doc.html",  // 从本行开端,以下是新增
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs",
                "/favicon.ico"
        };
        // 装备各恳求途径的认证与授权
        http.authorizeRequests() // 恳求需求授权才干够拜访
                .antMatchers(urls) // 匹配一些途径
                .permitAll() // 答应直接拜访(不需求经过认证和授权)
                .anyRequest() // 匹配除了以上装备的其它恳求
                .authenticated(); // 都需求认证
        // 注册处理JWT的过滤器
        // 此过滤器有必要在Spring Security处理登录的过滤器之前
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

咱们重起项目,输入之前的url,不太对啊,下载了一个空的login文件,操控台看到了如下内容:

JwtAuthenticationFilter.doFilterInternal()

那是由于过滤器的作业还没有完毕,他还需求完成以下功用:

  • 测验从恳求头中获取JWT数据,假如无JWT数据,直接放行,Spring Security会进行下一步处理,比方,白名单的恳求答应拜访,其它恳求制止拜访
  • 假如存在JWT数据,应该测验解析,解析失利,便是认证失利了,要求客户端从头登录,客户端就能够得到新的、正确的JWT,客户端鄙人一次提交恳求时,运用新的JWT就能够正常拜访
  • 将解析得到的数据封装到Authentication目标中,Spring Security的上下文中存储的数据类型便是Authentication类型
  • 为防止存入1次后,Spring Security的上下文中一直存在Authentication,在此过滤器履行的第一时刻,应该先铲除上一次的数据

下面,咱们来看看自界说过滤器中还有哪些代码:

package com.codingfire.cloud.passport.security;
import com.alibaba.fastjson.JSON;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.commons.restful.ResponseCode;
import io.jsonwebtoken.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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;
/**
 * JWT过滤器:从恳求头的Authorization中获取JWT中存入的用户信息
 * 并增加到Spring Security的上下文中
 * 以致于Spring Security后续的组件(包含过滤器等)能从上下文中获取此用户的信息
 * 从而验证是否现已登录、是否具有权限等
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    /**
     * JWT数据的密钥
     */
    private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthenticationFilter.doFilterInternal()");
        // 铲除Spring Security上下文中的数据
        // 防止此前从前存入过用户信息,后续即便没有带着JWT,在Spring Security仍保存有上下文数据(包含用户信息)
        System.out.println("铲除Spring Security上下文中的数据");
        SecurityContextHolder.clearContext();
        // 客户端提交恳求时,有必要在恳求头的Authorization中增加JWT数据,这是当时服务器程序的规定,客户端有必要遵守
        // 测验获取JWT数据
        String jwt = request.getHeader("Authorization");
        System.out.println("从恳求头中获取到的JWT=" + jwt);
        // 判断是否不存在jwt数据
        if (!StringUtils.hasText(jwt)) {
            // 不存在jwt数据,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则要求登录等
            // 此处不宜直接阻止运转,由于“登录”、“注册”等恳求本应该没有jwt数据
            System.out.println("恳求头中无JWT数据,当时过滤器将放行");
            filterChain.doFilter(request, response); // 持续履行过滤器链中后续的过滤器
            return; // 有必要
        }
        // 留意:此刻履行时,假如恳求头中带着了Authentication,日志中将输出,且不会有任何呼应,由于当时过滤器尚未放行
        // 以下代码有或许抛出反常的
        // TODO 密钥和各个Key应该一致界说
        String username = null;
        String permissionsString = null;
        try {
            System.out.println("恳求头中包含JWT,预备解析此数据……");
            Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
            username = claims.get("username").toString();
            permissionsString = claims.get("permissions").toString();
            System.out.println("username=" + username);
            System.out.println("permissionsString=" + permissionsString);
        } catch (ExpiredJwtException e) {
            System.out.println("解析JWT失利,此JWT已过期:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_EXPIRED, "您的登录已过期,请从头登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("呼应成果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (MalformedJwtException e) {
            System.out.println("解析JWT失利,此JWT数据错误,无法解析:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_MALFORMED, "获取登录信息失利,请从头登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("呼应成果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (SignatureException e) {
            System.out.println("解析JWT失利,此JWT签名错误:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_SIGNATURE, "获取登录信息失利,请从头登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("呼应成果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (Throwable e) {
            System.out.println("解析JWT失利,反常类型:" + e.getClass().getName());
            e.printStackTrace();
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_INTERNAL_SERVER_ERROR, "获取登录信息失利,请从头登录!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("呼应成果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }
        // 将此前从JWT中读取到的permissionsString(JSON字符串)转化成Collection<? extends GrantedAuthority>
        List<SimpleGrantedAuthority> permissions
                = JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
        System.out.println("从JWT中获取到的权限转化成Spring Security要求的类型:" + permissions);
        // 将解析得到的用户信息传递给Spring Security
        // 获取Spring Security的上下文,并将Authentication放到上下文中
        // 在Authentication中封装:用户名、null(暗码)、权限列表
        // 由于接下来并不会处理认证,所以Authentication中不需求暗码
        // 后续,Spring Security发现上下文中有Authentication时,就会视为已登录,乃至能够获取相关信息
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(username, null, permissions);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        System.out.println("将解析得到的用户信息传递给Spring Security");
        // 放行
        System.out.println("JwtAuthenticationFilter 放行");
        filterChain.doFilter(request, response);
    }
}

你或许在增加了这个类中的代码后会有一些报错,是由于错误码没有提早声明在枚举类,自己手动增加一下。

接着在SecurityConfiguration类上增加一个新的注解

@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增

作用是开启“经过注解装备权限”的功用。

下面,咱们来做个测验,在任何你需求设置权限的处理恳求的办法上,经过@PreAuthorize注解来完成经过注解装备权限功用,你能够装备你想要的某种权限:

在AdminController类中增加如下办法:

    @GetMapping("/codingfire")
    @PreAuthorize("hasAuthority('单频道观看')") // 新增
    public String codingfire() {
        return "codingfire";
    }

重启项目,运用具有“单频道观看”权限的用户能够直接拜访,不具有此权限的用户则不能拜访,将呈现403错误,可经过在线文档功用进行测验。

在线文档增加恳求头办法:

硬核!字节2023版Spring全家桶进阶笔记流出,堪称Java跳槽神器

恳求头内的数据运用正常用的登录后回来的JWT数据,登录的用户权限可自己调整,然后拜访codingfire接口检查成果。博主就不再贴后续的内容了。

结语

尽管这篇博客完毕了,但登录并没有完毕,登录的整体逻辑还有不少,要害的部分本文现已全部列出,剩余的就要咱们在实战中渐渐叠加了。3w字才码完,能够说是自己又学习了一遍,你会发现,许多东西都是套路的固定的,只有少部分东西是需求自己去写的,那便是涉及事务的部分。希望咱们都能有所收成。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。