国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Java開(kāi)發(fā) - 單點(diǎn)登錄初體驗(yàn)(Spring Security + JWT)

這篇具有很好參考價(jià)值的文章主要介紹了Java開(kāi)發(fā) - 單點(diǎn)登錄初體驗(yàn)(Spring Security + JWT)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

目錄???????

前言

為什么要登錄

登錄的種類(lèi)

Cookie-Session

Cookie-Session-local storage

JWT令牌

幾種登陸總結(jié)?

用戶(hù)身份認(rèn)證與授權(quán)

創(chuàng)建工程

添加依賴(lài)

啟動(dòng)項(xiàng)目

Bcrypt算法的工具

創(chuàng)建VO模型類(lèi)

創(chuàng)建接口文件

創(chuàng)建XML文件

補(bǔ)充配置

添加依賴(lài)

添加配置

創(chuàng)建配置類(lèi)

測(cè)試上面的配置

讓Spring Security通過(guò)數(shù)據(jù)庫(kù)驗(yàn)證密碼

配置密碼加密器

重寫(xiě)Spring Security下的用戶(hù)相關(guān)抽象方法

測(cè)試成果

JWT

什么是JWT

為什么使用JWT

如何使用JWT

添加依賴(lài)

測(cè)試jwt

在Spring Security中使用JWT

自動(dòng)裝配AuthenticationManager對(duì)象

創(chuàng)建DTO類(lèi)

創(chuàng)建接口類(lèi)

創(chuàng)建實(shí)現(xiàn)類(lèi)

創(chuàng)建控制器類(lèi)

測(cè)試代碼

返回客戶(hù)端JWT數(shù)據(jù)

修改AdminServiceImpl實(shí)現(xiàn)類(lèi)

修改控制器類(lèi)

測(cè)試jwt數(shù)據(jù)返回

使用其他URL被屏蔽怎么辦

使用請(qǐng)求頭

結(jié)語(yǔ)


前言

登錄這東西很奇怪哎,你說(shuō)它難嗎?好像客戶(hù)端只需要調(diào)接口就行,那有啥難的?當(dāng)你多多少少對(duì)登錄的后臺(tái)有些了解,又覺(jué)得好難啊,session,token,cookie,等等一堆東西,有老的大家都不喜歡用的,有新的一些不太懂的,根據(jù)公司項(xiàng)目規(guī)模不同,還要考慮成本的問(wèn)題,真是有些頭疼。博主今天推薦的一種登陸方式便是Spring Security + JWT的結(jié)合使用,為什么要兩者結(jié)合呢?Spring Security現(xiàn)在已經(jīng)很少用了,甚至有些人認(rèn)為已經(jīng)廢棄了,但是因?yàn)镾pring Security是Spring系列的東西,Spring對(duì)其支持很友好,不,是非常友好。但是我們不想使用他驗(yàn)證后的操作,所以我們要打斷這個(gè)操作,讓JWT工作。下面我們就來(lái)了解并手動(dòng)操作一下吧,本篇還是集成在我們前面的微服務(wù)項(xiàng)目中,你也可以另起項(xiàng)目一起來(lái)做。

為什么要登錄

我們平時(shí)都知道登錄,不知道有沒(méi)有思考過(guò)登錄解決的是什么問(wèn)題?大家會(huì)想到,不登錄就不能拿到用戶(hù)信息,一些用戶(hù)行為和服務(wù)就沒(méi)有辦法關(guān)聯(lián)到具體人身上,沒(méi)錯(cuò)!比如購(gòu)買(mǎi)行為。但我覺(jué)得這個(gè)說(shuō)法不夠具體,登錄的具體作用應(yīng)該是拿到用戶(hù)的權(quán)限。

我們說(shuō),是人,就有不同的角色,一個(gè)男人,可以是兒子,可以是父親,可以是員工,可以是老板等等。那我們就認(rèn)為,一個(gè)登錄體系中,必須要有一張用戶(hù)表和一張角色表,還有剛剛說(shuō)的權(quán)限表。這三張表之間還需要有表明其關(guān)系的關(guān)聯(lián)表。

可以說(shuō),這三張表在任何一個(gè)登錄體系都是必備的,甚至你還可以有臨時(shí)的用戶(hù)權(quán)限表,他們之間多是多對(duì)多的關(guān)系,要理清他們之間的關(guān)系并不簡(jiǎn)單。

登錄的種類(lèi)

登錄的種類(lèi)到目前為止所使用的技術(shù)大概五六種吧,其之間大同小異,從早期的Cookie-Session到現(xiàn)在的單點(diǎn)登錄,中間跨越的時(shí)間不短,其中有一個(gè)時(shí)間分割點(diǎn)就是html5標(biāo)準(zhǔn)出現(xiàn)的時(shí)候,他帶來(lái)了local storage,使得跨域問(wèn)題得到良好的解決,但我們并不滿(mǎn)足于這種方式,于是token技術(shù)出現(xiàn),但是本質(zhì)上和基于Cookie-Session+local storage的方式?jīng)]有太大區(qū)別。為了解決微服務(wù)間的數(shù)據(jù)同步,基于Token的JWT認(rèn)證誕生,其中還有一種利用session和redis的數(shù)據(jù)共享技術(shù)也能實(shí)現(xiàn)數(shù)據(jù)共享,這和token技術(shù)也類(lèi)似。接下來(lái),我們來(lái)簡(jiǎn)單的了解一下這幾種登錄方式:

Cookie-Session

這種方式要追溯到html5出現(xiàn)之前,那時(shí)只能利用cookie存儲(chǔ)SessionId,但cookie在跨域問(wèn)題上一言難盡,但并不是不能跨域,只是要比我們后面的方法麻煩,有l(wèi)ocal storage你還用cookie?而且cookie退出站點(diǎn)后就會(huì)銷(xiāo)毀,這點(diǎn)讓人極不能接受。其流程是:

  • 用戶(hù)輸入用戶(hù)名、密碼或短信驗(yàn)證碼登錄
  • 服務(wù)端驗(yàn)證后,返回一個(gè) SessionId,其和用戶(hù)信息關(guān)聯(lián)??蛻?hù)端將 SessionID存到cookie
  • 當(dāng)客戶(hù)端再發(fā)起請(qǐng)求時(shí)帶上cookie中的信息,服務(wù)端通過(guò)cookie獲取 SessionID并校驗(yàn),以判斷用戶(hù)是否登錄
    ?

Cookie-Session-local storage

這種方式和以上相似,只是改了幾個(gè)地方:

  • 存儲(chǔ)的位置不再是cookie,而是local storage
  • 服務(wù)端不存儲(chǔ)sessionid,而是改用redis做存儲(chǔ),可以解決同步問(wèn)題,但也有缺點(diǎn),同步會(huì)造成數(shù)據(jù)量增加,占用額外內(nèi)存,我們通過(guò)一張圖來(lái)說(shuō)明

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

左邊先行,獲取用戶(hù)信息,生成sessionid,存儲(chǔ)在redis,右邊訪(fǎng)問(wèn)其他模塊,通過(guò)sessionid去redis拿用戶(hù)信息,注意,用戶(hù)模塊和其他模塊也會(huì)保存sessionid,這就是數(shù)據(jù)共享,用戶(hù)量很大的情況會(huì)造成數(shù)據(jù)冗余,不適合用戶(hù)量特別大的項(xiàng)目,中小型項(xiàng)目可以。對(duì)于客戶(hù)端,sessionid當(dāng)然是保存在local storage內(nèi)了,畢竟誰(shuí)也不想去額外解決跨域的問(wèn)題。

JWT令牌

這種方式是目前使用比較多的一種方式,它和上面的方式也有相似之處,只是少了數(shù)據(jù)的存儲(chǔ),JWT不存儲(chǔ)session這些東西,它只負(fù)責(zé)驗(yàn)證jwt是否正確,驗(yàn)證的過(guò)程就是解碼的過(guò)程,關(guān)于JWT的標(biāo)準(zhǔn)制式的解釋?zhuān)?qǐng)大家手動(dòng)百度吧,不再贅述,博主也記不住,貼了浪費(fèi)篇幅??纯?,大概知道是怎么做的就行。

此處必須有圖:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

服務(wù)端不保存信息,這一點(diǎn)可以節(jié)省空間,誰(shuí)的信息誰(shuí)自己保存,解密方式在我這里,同時(shí)提高了安全性,何樂(lè)不為?

幾種登陸總結(jié)?

如果細(xì)分還能再分出幾種登錄方式,但基本大同小異,博主合并了其中相似的登錄方式,總結(jié)出來(lái)這三種,此處忽略第三方登錄,可自行了解??隙ㄟ€有其他方式,但總的來(lái)說(shuō),和這三種應(yīng)該是類(lèi)似,并不會(huì)完全不同??戳艘黄狾Auth2.0單點(diǎn)登錄相關(guān)的文章,還有一篇總結(jié)登錄的文章,真是寫(xiě)的太好了,分享給大家:

安全驗(yàn)證 - 知乎

Java——項(xiàng)目常用登錄方式詳解_new 海綿寶寶()的博客-CSDN博客

里面總結(jié)的很全面,也有一些案例,初學(xué)者可以看看。

用戶(hù)身份認(rèn)證與授權(quán)

從這里開(kāi)始,就是我們的項(xiàng)目時(shí)間,首先出場(chǎng)的是Spring Security,它是用于解決認(rèn)證與授權(quán)的框架。Spring Security有默認(rèn)的登錄賬號(hào)和密碼,用戶(hù)名user,密碼是隨機(jī)的,每次啟動(dòng)項(xiàng)目都會(huì)重新生成一個(gè)。它要求所有的請(qǐng)求都必須先登錄才允許訪(fǎng)問(wèn),稍后我們集成后可以來(lái)進(jìn)行測(cè)試。

創(chuàng)建工程

在微服務(wù)項(xiàng)目cloud下創(chuàng)建cloud-passport子項(xiàng)目:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

添加依賴(lài)

<?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:處理認(rèn)證與授權(quán) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring Boot Test:測(cè)試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

父子關(guān)聯(lián)

<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>

啟動(dòng)項(xiàng)目

依賴(lài)添加完畢,什么都不需要做,直接運(yùn)行passport的啟動(dòng)文件,可以在控制臺(tái)看到如下輸出:

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

這就是Spring Security生成的隨機(jī)密碼,它同時(shí)還提供了一個(gè)URL:http://localhost:8080/login?

我們點(diǎn)開(kāi)URL,在瀏覽器打開(kāi)一個(gè)登錄頁(yè)面,我們輸入用戶(hù)名:user,密碼就用上面的密碼,登錄成功后跳轉(zhuǎn)回之前訪(fǎng)問(wèn)的URL,由于我們沒(méi)有做這個(gè)頁(yè)面,會(huì)顯示404。這就是Spring Security默認(rèn)要求所有的請(qǐng)求都是必須先登錄才允許的訪(fǎng)問(wèn)的能力。

Bcrypt算法的工具

Spring Security的依賴(lài)項(xiàng)中包括了Bcrypt算法的工具類(lèi),這是一款非常優(yōu)秀的密碼加密工具,適和對(duì)需要存儲(chǔ)下來(lái)的密碼進(jìn)行加密處理。我們來(lái)測(cè)試下看看。

打開(kāi)測(cè)試類(lèi),添加如下測(cè)試代碼:

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);
    }
}

我們分別運(yùn)行這兩個(gè)方法,會(huì)看到如下輸出:

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

下面的解密使用上面的第一個(gè)加密后的密文進(jìn)行的解密。大家要用自己的電腦生成的密文進(jìn)行解密,用博主的可能會(huì)出現(xiàn)無(wú)法匹配的情況。

此加密工具有個(gè)特點(diǎn),大家應(yīng)該發(fā)現(xiàn)了,此加密得到的密文都不相同。

接著需要和數(shù)據(jù)庫(kù)中存儲(chǔ)的密文進(jìn)行對(duì)比,此時(shí)需要使用SQL去數(shù)據(jù)庫(kù)查詢(xún)?cè)撚脩?hù)的密文進(jìn)行比對(duì),比對(duì)通過(guò),則可進(jìn)行登錄。此時(shí)就不能使用默認(rèn)的user用戶(hù)名和隨機(jī)的密碼的方式,具體做法我們繼續(xù)往下看。

創(chuàng)建VO模型類(lèi)

在commons工程下創(chuàng)建pojo.passport.vo.AdminLoginVO類(lèi):

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,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;
}

?創(chuàng)建完成后我們發(fā)現(xiàn)要使用commons模塊,那需要依賴(lài)添加此模塊:

<!--all-common依賴(lài)-->
<dependency>
    <groupId>com.codingfire</groupId>
    <artifactId>cloud-commons</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

創(chuàng)建接口文件

在passport下創(chuàng)建mapper.AdminMapper接口:

package com.codingfire.cloud.passport.mapper;

import com.codingfire.cloud.commons.pojo.passport.vo.AdminLoginVO;

public interface AdminMapper {
    AdminLoginVO getLoginInfoByUsername(String username);
}

創(chuàng)建XML文件

大家還記得嗎?我們?cè)贛ybatis框架中有使用XML文件來(lái)寫(xiě)SQL。在src/main/resources下創(chuàng)建mapper文件夾,mapper文件夾下可以把前面的xml文件粘貼過(guò)來(lái),寫(xiě)入如下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>

在這里大家要留意幾個(gè)問(wèn)題了,我們這里需要連接mybatis的數(shù)據(jù)庫(kù),第一次看博主文章的需要看看Java開(kāi)發(fā) - Mybatis框架初體驗(yàn)_CodingFire的博客-CSDN博客

這篇博客,才知道建的什么數(shù)據(jù)庫(kù), 有哪些表,有哪些參數(shù),否則將很難進(jìn)行下去。

補(bǔ)充配置

由于需要使用數(shù)據(jù)庫(kù),需要補(bǔ)充配置和依賴(lài)。

添加依賴(lài)

<!--mybatis整合springboot-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--alibaba 數(shù)據(jù)源德魯伊-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--mysql驅(qū)動(dòng)-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

添加配置

這里,我們選擇從mybatis復(fù)制配置信息到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

密碼寫(xiě)自己的數(shù)據(jù)庫(kù)密碼。

創(chuàng)建配置類(lèi)

需要連接數(shù)據(jù)庫(kù),那么少不了mybatis配置了,創(chuàng)建MybatisConfiguration類(lèi),在passport下創(chuàng)建config包,此包下創(chuàng)建配置類(lèi):

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 {
}

前面也是有創(chuàng)建過(guò)的,你可以選擇直接貼過(guò)來(lái),但要注意掃描的路徑改成自己的路徑。原本需要在配置文件中配置mybatis.mapper-locations屬性,上面已經(jīng)補(bǔ)充過(guò)了。

測(cè)試上面的配置

在測(cè)試類(lèi)下,我們添加如下代碼:

    @Autowired
    AdminMapper adminMapper;

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

這是我們?cè)瓉?lái)表中的數(shù)據(jù),沒(méi)有數(shù)據(jù)的需要預(yù)先插入一些數(shù)據(jù)。運(yùn)行測(cè)試方法,發(fā)現(xiàn)報(bào)錯(cuò)?

額,一大堆,看了......好一會(huì)兒,才發(fā)現(xiàn)有兩個(gè)地方寫(xiě)錯(cuò)了:

一個(gè)是AdminMapper內(nèi)沒(méi)有添加@Repository注解:onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

另一個(gè)是MybatisConfiguration類(lèi)上的scan注解寫(xiě)錯(cuò)了,修改一下:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

然后再次運(yùn)行測(cè)試方法,可以在控制臺(tái)看到輸出的用戶(hù)信息如下:

AdminLoginVO(id=1, username=admin04, password=123456, isLogin=0, permissions=[全頻道可刪除, 全頻道可篩選, 全頻道讀取, 單頻道可刪除, 單頻道可篩選, 單頻道觀看])?

?代表我們的測(cè)試成功了。簡(jiǎn)直累的一逼,真是錯(cuò)一步都不行。

讓Spring Security通過(guò)數(shù)據(jù)庫(kù)驗(yàn)證密碼

前面提過(guò),要讓Spring Security通過(guò)數(shù)據(jù)庫(kù)的數(shù)據(jù)來(lái)驗(yàn)證用戶(hù)名與密碼,我們還需要做出一些修改和配置,我們看到每次控制臺(tái)都會(huì)輸出一串新的密碼:

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

下面,讓我們看看怎樣才能不讓它輸出。

配置密碼加密器

在config包下創(chuàng)建SecurityConfiguration類(lèi):

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();
    }

}

重寫(xiě)Spring Security下的用戶(hù)相關(guān)抽象方法

在passport下建新包security,包下建類(lèi)UserDetailsServiceImpl,并實(shí)現(xiàn)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("根據(jù)用戶(hù)名查詢(xún)嘗試登錄的管理員信息,用戶(hù)名=" + s);
        AdminLoginVO admin = adminMapper.getLoginInfoByUsername(s);
        System.out.println("通過(guò)持久層進(jìn)行查詢(xún),結(jié)果=" + admin);

        if (admin == null) {
            System.out.println("根據(jù)用戶(hù)名沒(méi)有查詢(xún)到有效的管理員數(shù)據(jù),將拋出異常");
            throw new BadCredentialsException("登錄失敗,用戶(hù)名不存在!");
        }

        System.out.println("查詢(xún)到匹配的管理員數(shù)據(jù),需要將此數(shù)據(jù)轉(zhuǎn)換為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("轉(zhuǎn)換得到UserDetails=" + userDetails);
        return userDetails;
    }

}

測(cè)試成果

重新啟動(dòng)工程,看看還有沒(méi)有隨機(jī)密碼生成:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

可以看到,隨機(jī)密碼已經(jīng)不會(huì)再自動(dòng)生成。

JWT

什么是JWT

Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開(kāi)放標(biāo)準(zhǔn)((RFC 7519).定義了一種簡(jiǎn)潔的,自包含的方法用于通信雙方之間以JSON對(duì)象的形式安全的傳遞信息。因?yàn)閿?shù)字簽名的存在,這些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘鑰對(duì)進(jìn)行簽名。

客戶(hù)端第1次訪(fǎng)問(wèn)服務(wù)器端時(shí),是沒(méi)有攜帶令牌訪(fǎng)問(wèn)的,當(dāng)服務(wù)器進(jìn)行響應(yīng)時(shí),會(huì)將JWT響應(yīng)到客戶(hù)端,客戶(hù)端保存后,在第2次訪(fǎng)問(wèn)時(shí)就開(kāi)始攜帶JWT進(jìn)行請(qǐng)求,服務(wù)器收到請(qǐng)求中的JWT后就可以識(shí)別用戶(hù)身份。

關(guān)于JWT的詳細(xì)介紹,推薦這篇博客:SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證 - 簡(jiǎn)書(shū)

為什么使用JWT

Spring Security默認(rèn)使用Session機(jī)制存儲(chǔ)用戶(hù)信息,而HTTP協(xié)議是無(wú)狀態(tài)協(xié)議,它不保存客戶(hù)端信息,所以,同一個(gè)客戶(hù)端的多次訪(fǎng)問(wèn),等效于多個(gè)不同的客戶(hù)端各訪(fǎng)問(wèn)一次服務(wù)端,為了保存用戶(hù)信息,使服務(wù)器端能夠識(shí)別客戶(hù)端身份,我們推薦使用Token或其他技術(shù),比如我們馬上要說(shuō)的JWT。

如何使用JWT

添加依賴(lài)

JWT只是一個(gè)概念,而實(shí)現(xiàn)生成JWT、解析JWT的框架卻有不少,我們這里要使用的是jjwt,添加依賴(lài)如下:

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

由于版本已經(jīng)在主項(xiàng)目中控制,此處版本省略。

測(cè)試jwt

在測(cè)試類(lèi)下創(chuàng)建JwtTests類(lèi),添加如下測(cè)試代碼:

// 密鑰,遵從越長(zhǎng)越好,越亂越復(fù)雜越好的原則
    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:指定算法與當(dāng)前數(shù)據(jù)類(lèi)型
                // 格式為: { "alg": 算法, "typ": "jwt" }
                .setHeaderParam(Header.CONTENT_TYPE, "HS256")
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                // Payload:通常包含Claims(自定義數(shù)據(jù))和過(guò)期時(shí)間
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                // Signature:由算法和密鑰(secret key)這2部分組成
                .signWith(SignatureAlgorithm.HS256, secretKey)
                // 打包生成
                .compact();
        System.out.println(jwt);
    }

運(yùn)行測(cè)試方法,輸出加密后的密文如下:

eyJjdHkiOiJIUzI1NiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJuYW1lIjoiY29kaW5nZmlyZSIsImlkIjoxLCJleHAiOjE2Nzc3MzgyNzZ9.cz_cjbIT28GgZ5gQFkgOEAVmqjqFRW2MIliGftfT2As

你能看到里面有兩個(gè)點(diǎn),這是JWT加密的固定格式,需要你去看推薦的博文。

接著我們把這串密文用來(lái)解密試試看能得到什么:

    @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);
    }

運(yùn)行測(cè)試方法:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

看到如圖所示結(jié)果,你的jwt就已經(jīng)引入成功。但,這還不夠,我們是要在Spring Security中使用JWT,所以還有很多工作要做。

在Spring Security中使用JWT

自動(dòng)裝配AuthenticationManager對(duì)象

這是一個(gè)認(rèn)證管理器,我們需要接管這個(gè)管理器,在SecurityConfiguration類(lèi)中做一些操作,來(lái)看看最終的SecurityConfiguration類(lèi)吧:

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"
        };

        // 配置各請(qǐng)求路徑的認(rèn)證與授權(quán)
        http.authorizeRequests() // 請(qǐng)求需要授權(quán)才可以訪(fǎng)問(wèn)
                .antMatchers(urls) // 匹配一些路徑
                .permitAll() // 允許直接訪(fǎng)問(wèn)(不需要經(jīng)過(guò)認(rèn)證和授權(quán))
                .anyRequest() // 匹配除了以上配置的其它請(qǐng)求
                .authenticated(); // 都需要認(rèn)證
    }
}

創(chuàng)建DTO類(lèi)

在上面創(chuàng)建AdminLoginVO類(lèi)的地方創(chuàng)建新的包dto,下面建新類(lèi):

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

創(chuàng)建接口類(lèi)

在passport下創(chuàng)建service包,其下創(chuàng)建新接口類(lèi)IAdminService:

package com.codingfire.cloud.passport.service;

import com.codingfire.cloud.commons.pojo.passport.dto.AdminLoginDTO;

public interface IAdminService {

    String login(AdminLoginDTO adminLoginDTO);

}

創(chuàng)建實(shí)現(xiàn)類(lèi)

在service包下創(chuàng)建新包impl,其下創(chuàng)建實(shí)現(xiàn)類(lèi)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) {

        // 生成此用戶(hù)數(shù)據(jù)的JWT
        String jwt = "This is a JWT."; // 臨時(shí)
        return jwt;
    }
}

創(chuàng)建控制器類(lèi)

在passport下創(chuàng)建controller包,其下創(chuàng)建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;
    }

}

測(cè)試代碼

啟動(dòng)項(xiàng)目,在瀏覽器輸入:http://localhost:8080/admins/login?username= codingfire&password=123456??

把用戶(hù)名和密碼改成你自己數(shù)據(jù)庫(kù)中的用戶(hù)名和密碼,也可以寫(xiě)錯(cuò)的,然后進(jìn)行多次嘗試,看瀏覽器會(huì)返回什么:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

?看到此信息,就代表你的jwt接入成功了,但我們需要返回給客戶(hù)端jwt數(shù)據(jù),接下來(lái)我們實(shí)現(xiàn)這個(gè)過(guò)程。

返回客戶(hù)端JWT數(shù)據(jù)

修改AdminServiceImpl實(shí)現(xiàn)類(lèi)

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) {
        // 密鑰,遵從越長(zhǎng)越好,越亂越復(fù)雜越好的原則
        String secretKey = "asjdkahwehuqdyaoisdqwuphdabskbkansdjashdjasdh";

        // 準(zhǔn)備被認(rèn)證數(shù)據(jù)
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(
                adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
        // 調(diào)用AuthenticationManager驗(yàn)證用戶(hù)名與密碼
        // 執(zhí)行認(rèn)證,如果此過(guò)程沒(méi)有拋出異常,則表示認(rèn)證通過(guò),如果認(rèn)證信息有誤,將拋出異常
        authenticationManager.authenticate(authentication);

        User user = (User) authentication.getPrincipal();
        System.out.println("從認(rèn)證結(jié)果中獲取Principal=" + user.getClass().getName());
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", user.getUsername());
        claims.put("permissions", user.getAuthorities());
        System.out.println("即將向JWT中寫(xiě)入數(shù)據(jù)=" + claims);

        // JWT的組成部分:Header(頭),Payload(載荷),Signature(簽名)
        String jwt = Jwts.builder()
                // Header:指定算法與當(dāng)前數(shù)據(jù)類(lèi)型
                // 格式為: { "alg": 算法, "typ": "jwt" }
                .setHeaderParam(Header.CONTENT_TYPE, "HS256")
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                // Payload:通常包含Claims(自定義數(shù)據(jù))和過(guò)期時(shí)間
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
                // Signature:由算法和密鑰(secret key)這2部分組成
                .signWith(SignatureAlgorithm.HS256, secretKey)
                // 打包生成
                .compact();

        // 返回JWT數(shù)據(jù)
        return jwt;
    }
}

?你會(huì)發(fā)現(xiàn),這就是我們?cè)跍y(cè)試類(lèi)中測(cè)試的代碼,基本上是直接貼過(guò)來(lái)的。

修改控制器類(lèi)

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);
    }

}

修改返回值類(lèi)型。

測(cè)試jwt數(shù)據(jù)返回

運(yùn)行項(xiàng)目,在瀏覽器輸入原來(lái)的html:?http://localhost:8080/admins/login?username= codingfire&password=123456?

瀏覽器將得到如下數(shù)據(jù):

{"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}

這里有個(gè)坑啊小伙伴們,如果你一直403,且控制臺(tái)提示你Encoded password does not look like BCrypt,這是因?yàn)槟愕臄?shù)據(jù)庫(kù)存儲(chǔ)的是明文密碼,必須存儲(chǔ)我們?cè)跍y(cè)試類(lèi)中使用BCryptPasswordEncoder加密后的密碼。博主剛剛就犯了這個(gè)錯(cuò),真實(shí)太容易忽略了,不知道該說(shuō)啥,大家可別犯這個(gè)錯(cuò)。

使用其他URL被屏蔽怎么辦

剛剛由于我們禁止了未登陸時(shí)直接進(jìn)入Spring Security的登陸頁(yè),所以才需要添加了白名單解決屏蔽所有連接的問(wèn)題。如果使用Knife4j,該怎么添加白名單呢?我們來(lái)看看:

        String[] urls = {
                "/admins/login",
                "/doc.html",  // 從本行開(kāi)始,以下是新增
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs",
                "/favicon.ico"
        };

使用請(qǐng)求頭

得到JWT之后,在后續(xù)的請(qǐng)求中都需要在請(qǐng)求頭中帶上JWT,放在Authorization屬性?xún)?nèi),所以應(yīng)該先判斷請(qǐng)求頭中是否有Authorization,而不能讓請(qǐng)求直達(dá)服務(wù)器業(yè)務(wù)模塊。這讓我想到了前面講過(guò)的過(guò)濾器,下面,我們?cè)趕ecurity包下創(chuàng)建一個(gè)過(guò)濾器類(lèi):

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()");
    }

}

過(guò)濾器類(lèi)是需要注冊(cè)后才能工作的,所以下一步對(duì)過(guò)濾器進(jìn)行注冊(cè)。用于驗(yàn)證JWT的過(guò)濾器應(yīng)該運(yùn)行在Spring Security處理登錄的過(guò)濾器之前才能工作,所以需要在自定義的SecurityConfiguration中的configure()方法中將我們自定義的過(guò)濾器注冊(cè)在Spring Security的相關(guān)過(guò)濾器之前。

同一個(gè)項(xiàng)目中允許存在多個(gè)過(guò)濾器,形成過(guò)濾器鏈,所以我們注冊(cè)過(guò)濾器不需要單獨(dú)建個(gè)類(lèi)來(lái)處理了,而是在SecurityConfiguration類(lèi)中進(jìn)行,最終的類(lèi)如下:

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",  // 從本行開(kāi)始,以下是新增
                "/**/*.js",
                "/**/*.css",
                "/swagger-resources",
                "/v2/api-docs",
                "/favicon.ico"
        };


        // 配置各請(qǐng)求路徑的認(rèn)證與授權(quán)
        http.authorizeRequests() // 請(qǐng)求需要授權(quán)才可以訪(fǎng)問(wèn)
                .antMatchers(urls) // 匹配一些路徑
                .permitAll() // 允許直接訪(fǎng)問(wèn)(不需要經(jīng)過(guò)認(rèn)證和授權(quán))
                .anyRequest() // 匹配除了以上配置的其它請(qǐng)求
                .authenticated(); // 都需要認(rèn)證

        // 注冊(cè)處理JWT的過(guò)濾器
        // 此過(guò)濾器必須在Spring Security處理登錄的過(guò)濾器之前
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

我們重起項(xiàng)目,輸入之前的url,不太對(duì)啊,下載了一個(gè)空的login文件,控制臺(tái)看到了如下內(nèi)容:

JwtAuthenticationFilter.doFilterInternal()

那是因?yàn)檫^(guò)濾器的工作還沒(méi)有結(jié)束,他還需要實(shí)現(xiàn)以下功能:

  • 嘗試從請(qǐng)求頭中獲取JWT數(shù)據(jù),如果無(wú)JWT數(shù)據(jù),直接放行,Spring Security會(huì)進(jìn)行下一步處理,比如,白名單的請(qǐng)求允許訪(fǎng)問(wèn),其它請(qǐng)求禁止訪(fǎng)問(wèn)
  • 如果存在JWT數(shù)據(jù),應(yīng)該嘗試解析,解析失敗,就是認(rèn)證失敗了,要求客戶(hù)端重新登錄,客戶(hù)端就可以得到新的、正確的JWT,客戶(hù)端在下一次提交請(qǐng)求時(shí),使用新的JWT就可以正常訪(fǎng)問(wèn)
  • 將解析得到的數(shù)據(jù)封裝到Authentication對(duì)象中,Spring Security的上下文中存儲(chǔ)的數(shù)據(jù)類(lèi)型就是Authentication類(lèi)型
  • 為避免存入1次后,Spring Security的上下文中始終存在Authentication,在此過(guò)濾器執(zhí)行的第一時(shí)間,應(yīng)該先清除上一次的數(shù)據(jù)

?下面,我們來(lái)看看自定義過(guò)濾器中還有哪些代碼:

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過(guò)濾器:從請(qǐng)求頭的Authorization中獲取JWT中存入的用戶(hù)信息
 * 并添加到Spring Security的上下文中
 * 以致于Spring Security后續(xù)的組件(包括過(guò)濾器等)能從上下文中獲取此用戶(hù)的信息
 * 從而驗(yàn)證是否已經(jīng)登錄、是否具有權(quán)限等
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    /**
     * JWT數(shù)據(jù)的密鑰
     */
    private String secretKey = "fgfdsfadsfadsafdsafdsfadsfadsfdsafdasfdsafdsafdsafds4rttrefds";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        System.out.println("JwtAuthenticationFilter.doFilterInternal()");
        // 清除Spring Security上下文中的數(shù)據(jù)
        // 避免此前曾經(jīng)存入過(guò)用戶(hù)信息,后續(xù)即使沒(méi)有攜帶JWT,在Spring Security仍保存有上下文數(shù)據(jù)(包括用戶(hù)信息)
        System.out.println("清除Spring Security上下文中的數(shù)據(jù)");
        SecurityContextHolder.clearContext();
        // 客戶(hù)端提交請(qǐng)求時(shí),必須在請(qǐng)求頭的Authorization中添加JWT數(shù)據(jù),這是當(dāng)前服務(wù)器程序的規(guī)定,客戶(hù)端必須遵守
        // 嘗試獲取JWT數(shù)據(jù)
        String jwt = request.getHeader("Authorization");
        System.out.println("從請(qǐng)求頭中獲取到的JWT=" + jwt);
        // 判斷是否不存在jwt數(shù)據(jù)
        if (!StringUtils.hasText(jwt)) {
            // 不存在jwt數(shù)據(jù),則放行,后續(xù)還有其它過(guò)濾器及相關(guān)組件進(jìn)行其它的處理,例如未登錄則要求登錄等
            // 此處不宜直接阻止運(yùn)行,因?yàn)椤暗卿洝?、“注?cè)”等請(qǐng)求本應(yīng)該沒(méi)有jwt數(shù)據(jù)
            System.out.println("請(qǐng)求頭中無(wú)JWT數(shù)據(jù),當(dāng)前過(guò)濾器將放行");
            filterChain.doFilter(request, response); // 繼續(xù)執(zhí)行過(guò)濾器鏈中后續(xù)的過(guò)濾器
            return; // 必須
        }

        // 注意:此時(shí)執(zhí)行時(shí),如果請(qǐng)求頭中攜帶了Authentication,日志中將輸出,且不會(huì)有任何響應(yīng),因?yàn)楫?dāng)前過(guò)濾器尚未放行
        // 以下代碼有可能拋出異常的
        // TODO 密鑰和各個(gè)Key應(yīng)該統(tǒng)一定義
        String username = null;
        String permissionsString = null;
        try {
            System.out.println("請(qǐng)求頭中包含JWT,準(zhǔn)備解析此數(shù)據(jù)……");
            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已過(guò)期:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_EXPIRED, "您的登錄已過(guò)期,請(qǐng)重新登錄!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("響應(yīng)結(jié)果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (MalformedJwtException e) {
            System.out.println("解析JWT失敗,此JWT數(shù)據(jù)錯(cuò)誤,無(wú)法解析:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_MALFORMED, "獲取登錄信息失敗,請(qǐng)重新登錄!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("響應(yīng)結(jié)果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (SignatureException e) {
            System.out.println("解析JWT失敗,此JWT簽名錯(cuò)誤:" + e.getMessage());
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_JWT_SIGNATURE, "獲取登錄信息失敗,請(qǐng)重新登錄!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("響應(yīng)結(jié)果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        } catch (Throwable e) {
            System.out.println("解析JWT失敗,異常類(lèi)型:" + e.getClass().getName());
            e.printStackTrace();
            JsonResult<Void> jsonResult = JsonResult.failed(
                    ResponseCode.ERR_INTERNAL_SERVER_ERROR, "獲取登錄信息失敗,請(qǐng)重新登錄!");
            String jsonString = JSON.toJSONString(jsonResult);
            System.out.println("響應(yīng)結(jié)果:" + jsonString);
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().println(jsonString);
            return;
        }

        // 將此前從JWT中讀取到的permissionsString(JSON字符串)轉(zhuǎn)換成Collection<? extends GrantedAuthority>
        List<SimpleGrantedAuthority> permissions
                = JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
        System.out.println("從JWT中獲取到的權(quán)限轉(zhuǎn)換成Spring Security要求的類(lèi)型:" + permissions);
        // 將解析得到的用戶(hù)信息傳遞給Spring Security
        // 獲取Spring Security的上下文,并將Authentication放到上下文中
        // 在Authentication中封裝:用戶(hù)名、null(密碼)、權(quán)限列表
        // 因?yàn)榻酉聛?lái)并不會(huì)處理認(rèn)證,所以Authentication中不需要密碼
        // 后續(xù),Spring Security發(fā)現(xiàn)上下文中有Authentication時(shí),就會(huì)視為已登錄,甚至可以獲取相關(guān)信息
        Authentication authentication
                = new UsernamePasswordAuthenticationToken(username, null, permissions);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        System.out.println("將解析得到的用戶(hù)信息傳遞給Spring Security");
        // 放行
        System.out.println("JwtAuthenticationFilter 放行");
        filterChain.doFilter(request, response);
    }

}

你可能在添加了這個(gè)類(lèi)中的代碼后會(huì)有一些報(bào)錯(cuò),是因?yàn)殄e(cuò)誤碼沒(méi)有提前聲明在枚舉類(lèi),自己手動(dòng)添加一下。

接著在SecurityConfiguration類(lèi)上添加一個(gè)新的注解

@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增

作用是開(kāi)啟“通過(guò)注解配置權(quán)限”的功能。

下面,我們來(lái)做個(gè)測(cè)試,在任何你需要設(shè)置權(quán)限的處理請(qǐng)求的方法上,通過(guò)@PreAuthorize注解來(lái)實(shí)現(xiàn)通過(guò)注解配置權(quán)限功能,你可以配置你想要的某種權(quán)限:

在AdminController類(lèi)中添加如下方法:

    @GetMapping("/codingfire")
    @PreAuthorize("hasAuthority('單頻道觀看')") // 新增
    public String codingfire() {
        return "codingfire";
    }

重啟項(xiàng)目,使用具有“單頻道觀看”權(quán)限的用戶(hù)可以直接訪(fǎng)問(wèn),不具有此權(quán)限的用戶(hù)則不能訪(fǎng)問(wèn),將出現(xiàn)403錯(cuò)誤,可通過(guò)在線(xiàn)文檔功能進(jìn)行測(cè)試。

在線(xiàn)文檔添加請(qǐng)求頭方式:

onceperrequestfilter能獲取到登錄的用戶(hù)名密碼嗎,Java之微服務(wù)簡(jiǎn)單上手系列,登錄,JWT,Spring Security,Token,Java登錄

請(qǐng)求頭內(nèi)的數(shù)據(jù)使用正常用的登錄后返回的JWT數(shù)據(jù),登錄的用戶(hù)權(quán)限可自己調(diào)整,然后訪(fǎng)問(wèn)codingfire接口查看結(jié)果。博主就不再貼后續(xù)的內(nèi)容了。?

結(jié)語(yǔ)

雖然這篇博客結(jié)束了,但登錄并沒(méi)有結(jié)束,登錄的整體邏輯還有不少,關(guān)鍵的部分本文已經(jīng)全部列出,剩下的就要大家在實(shí)戰(zhàn)中慢慢疊加了。3w字才碼完,可以說(shuō)是自己又學(xué)習(xí)了一遍,你會(huì)發(fā)現(xiàn),很多東西都是套路的固定的,只有少部分東西是需要自己去寫(xiě)的,那就是涉及業(yè)務(wù)的部分。希望大家都能有所收獲。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-782229.html

到了這里,關(guān)于Java開(kāi)發(fā) - 單點(diǎn)登錄初體驗(yàn)(Spring Security + JWT)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶(hù)投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • SpringCloud gateway+Spring Security + JWT實(shí)現(xiàn)登錄和用戶(hù)權(quán)限校驗(yàn)

    SpringCloud gateway+Spring Security + JWT實(shí)現(xiàn)登錄和用戶(hù)權(quán)限校驗(yàn)

    原本打算將Security模塊與gateway模塊分開(kāi)寫(xiě)的,但想到gateway本來(lái)就有過(guò)濾的作用 ,于是就把gateway和Security結(jié)合在一起了,然后結(jié)合JWT令牌對(duì)用戶(hù)身份和權(quán)限進(jìn)行校驗(yàn)。 Spring Cloud的網(wǎng)關(guān)與傳統(tǒng)的SpringMVC不同,gateway是基于Netty容器,采用的webflux技術(shù),所以gateway模塊不能引入spri

    2024年02月03日
    瀏覽(25)
  • SpringBoot+JWT實(shí)現(xiàn)單點(diǎn)登錄解決方案

    SpringBoot+JWT實(shí)現(xiàn)單點(diǎn)登錄解決方案

    一、什么是單點(diǎn)登錄? 單點(diǎn)登錄是一種統(tǒng)一認(rèn)證和授權(quán)機(jī)制,指在多個(gè)應(yīng)用系統(tǒng)中,用戶(hù)只需要登錄一次就可以訪(fǎng)問(wèn)所有相互信任的系統(tǒng),不需要重新登錄驗(yàn)證。 單點(diǎn)登錄一般用于互相授信的系統(tǒng),實(shí)現(xiàn)單一位置登錄,其他信任的應(yīng)用直接免登錄的方式,在多個(gè)應(yīng)用系統(tǒng)中,

    2024年02月12日
    瀏覽(21)
  • JWT 單點(diǎn)登錄探析:原理、用途與安全實(shí)踐

    JWT (JSON Web Token) 是目前最流行的跨域認(rèn)證解決方案,是 一種基于 Token 的認(rèn)證授權(quán)機(jī)制 。 從 JWT 的全稱(chēng)可以看出,JWT 本身也是 Token,一種規(guī)范化之后的 JSON 結(jié)構(gòu)的 Token。 通過(guò)數(shù)字簽名的方式,以 JSON 對(duì)象為載體,在不同的服務(wù)終端之間安全的傳輸信息。 JWT 自身包含了身

    2024年02月03日
    瀏覽(13)
  • token和JWT token區(qū)別、登錄安全、頁(yè)面權(quán)限、數(shù)據(jù)權(quán)限、單點(diǎn)登錄

    token和JWT token區(qū)別、登錄安全、頁(yè)面權(quán)限、數(shù)據(jù)權(quán)限、單點(diǎn)登錄

    ?直接區(qū)別: token需要查庫(kù)驗(yàn)證token 是否有效,而JWT不用查庫(kù)或者少查庫(kù),直接在服務(wù)端進(jìn)行校驗(yàn),并且不用查庫(kù)。因?yàn)橛脩?hù)的信息及加密信息在第二部分payload和第三部分簽證中已經(jīng)生成,只要在服務(wù)端進(jìn)行校驗(yàn)就行,并且校驗(yàn)也是JWT自己實(shí)現(xiàn)的。 ? ? JWT是json web token縮寫(xiě)。

    2023年04月09日
    瀏覽(26)
  • 【SpringBoot】集成SpringSecurity+JWT實(shí)現(xiàn)多服務(wù)單點(diǎn)登錄,原來(lái)這么easy

    【SpringBoot】集成SpringSecurity+JWT實(shí)現(xiàn)多服務(wù)單點(diǎn)登錄,原來(lái)這么easy

    Spring Boot+Spring Security+JWT實(shí)現(xiàn)單點(diǎn)登錄 介紹: 單點(diǎn)登錄(SingleSignOn,SSO) ,當(dāng)用戶(hù)在身份 認(rèn)證服務(wù)器 上登錄一次以后,即可 獲得訪(fǎng)問(wèn)單點(diǎn)登錄系統(tǒng)中其他關(guān)聯(lián)系統(tǒng)和應(yīng)用軟件的權(quán)限 ,同時(shí)這種實(shí)現(xiàn)是不需要管理員對(duì)用戶(hù)的登錄狀態(tài)或其他信息進(jìn)行修改的,這意味著在多個(gè)應(yīng)用

    2024年02月02日
    瀏覽(26)
  • 【Spring Security系列】Spring Security整合JWT:構(gòu)建安全的Web應(yīng)用

    【Spring Security系列】Spring Security整合JWT:構(gòu)建安全的Web應(yīng)用

    在企業(yè)級(jí)開(kāi)發(fā)或者我們自己的課程設(shè)計(jì)中,確保用戶(hù)數(shù)據(jù)的安全性和訪(fǎng)問(wèn)控制非常重要。而Spring Security和JWT是都兩個(gè)強(qiáng)大的工具,它倆結(jié)合可以幫助我們實(shí)現(xiàn)這一目標(biāo)。 Spring Security提供了全面的安全功能,而JWT則是一種用于身份驗(yàn)證的令牌機(jī)制。 前面兩個(gè)章節(jié)介紹過(guò)了Spri

    2024年04月23日
    瀏覽(24)
  • spring boot security使用jwt認(rèn)證

    spring boot security使用jwt認(rèn)證

    在前面的幾篇文章中: spring boot security快速使用示例 spring boot security之前后端分離配置 spring boot security自定義認(rèn)證 spring boot security驗(yàn)證碼登錄示例 基本對(duì)常用的基于cookie和session的認(rèn)證使用場(chǎng)景都已覆蓋。但是session屬于有狀態(tài)認(rèn)證,本文給出一個(gè)無(wú)狀態(tài)的認(rèn)證:jwt認(rèn)證示例。

    2024年02月12日
    瀏覽(18)
  • Spring Security詳細(xì)講解(JWT+SpringSecurity登入案例)

    Spring Security詳細(xì)講解(JWT+SpringSecurity登入案例)

    1.SpringSecurity SpringSecurity 是一個(gè)功能強(qiáng)大且高度可定制的身份驗(yàn)證和訪(fǎng)問(wèn)控制框架 。它是保護(hù)基于 Spring 的應(yīng)用程序的事實(shí)上的標(biāo)準(zhǔn)。 SpringSecurity 是一個(gè)致力于為 Java 應(yīng)用程序提供身份驗(yàn)證和授權(quán)的框架 。像所有 Spring 項(xiàng)目一樣,Spring Security 的真正強(qiáng)大之處在于它可以如何輕

    2024年02月02日
    瀏覽(23)
  • Spring Boot單點(diǎn)登錄實(shí)踐

    在現(xiàn)代的Web應(yīng)用程序中,單點(diǎn)登錄(Single Sign-On)已經(jīng)變得越來(lái)越流行。單點(diǎn)登錄使得用戶(hù)只需要一次認(rèn)證即可訪(fǎng)問(wèn)多個(gè)應(yīng)用程序,同時(shí)也提高了應(yīng)用程序的安全性。Spring Boot作為一種廣泛使用的Web開(kāi)發(fā)框架,在單點(diǎn)登錄方面也提供了很好的支持。 在本文中,我們將使用Spri

    2024年02月07日
    瀏覽(21)
  • spring-security -oauth2 整合 JWT

    spring-security -oauth2 整合 JWT

    在這個(gè)基礎(chǔ)上,進(jìn)行整合。 spring security oauth2學(xué)習(xí) -- 快速入門(mén)_本郡主是喵的博客-CSDN博客 先把? reids,common-pools? 等依賴(lài)刪掉。 刪掉redis的下相關(guān)配置 1.1?導(dǎo)入依賴(lài) 1.2 核心代碼 創(chuàng)建 jwtTokenConfig.java 在 AuthenticationServer.java 里面新增這些。 ?運(yùn)行,啟動(dòng)! ?復(fù)制這個(gè)token去官網(wǎng)解析

    2024年02月09日
    瀏覽(23)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包