2021最新Spring Security知識梳理
一、SpringSecurity 架構簡介
Spring 是非常流行和成功的 Java 應用開發架構,Spring Security 正是 Spring 家族中的成員。Spring Security 基于 Spring 架構,提供了一套 Web 應用安全性的完整解決方案。 正如你可能知道的關于安全方面的兩個主要區域是“認證”和“授權”(或者通路控制),一般來說,Web 應用的安全性包括使用者認證(Authentication)和使用者授權(Authorization)兩個部分,這兩點也是 Spring Security 重要核心功能。
1、使用者認證指的是:驗證某個使用者是否為系統中的合法主體,也就是說使用者能否通路該系統。使用者認證一般要求使用者提供使用者名和密碼。系統通過校驗使用者名和密碼來完成認證過程。通俗點說就是系統認為使用者是否能登入。
2、使用者授權指的是驗證某個使用者是否有權限執行某個操作。在一個系統中,不同使用者所具有的權限是不同的。比如對一個檔案來說,有的使用者隻能進行讀取,而有的使用者可以進行修改。一般來說,系統會為不同的使用者配置設定不同的角色,而每個角色則對應一系列的權限。通俗點講就是系統判斷使用者是否有權限去做某些事情。
整理了spring全家桶學習筆記,spring家族成員的學習資料都系統整理好了,需要的話可以直接點選領取
二、SpringSecurity與shiro
1、SpringSecurity特點
(1)和 Spring 無縫整合。
(2)全面的權限控制。
(3)專門為 Web 開發而設計。
- 舊版本不能脫離 Web 環境使用。
- 新版本對整個架構進行了分層抽取,分成了核心子產品和 Web 子產品。單獨
- 引入核心子產品就可以脫離 Web 環境。
- 重量級
2、shiro特點
Apache 旗下的輕量級權限控制架構。
(1)輕量級
Shiro 主張的理念是把複雜的事情變簡單。針對對性能有更高要求的網際網路應用有更好表現。
(2)通用性
好處:不局限于 Web 環境,可以脫離 Web 環境使用。
缺陷:在 Web 環境下一些特定的需求需要手動編寫代碼定制。
3、SpringSecurity與shiro總結
相對于 Shiro,在 SSM 中整合 Spring Security 都是比較麻煩的操作,是以,SpringSecurity 雖然功能比 Shiro 強大,但是使用反而沒有 Shiro 多(Shiro 雖然功能沒有Spring Security 多,但是對于大部分項目而言,Shiro 也夠用了)。自從有了 Spring Boot 之後,Spring Boot 對于 Spring Security 提供了自動化配置方案,可以使用更少的配置來使用 Spring Security。是以,一般來說,常見的安全管理技術棧的組合是這樣的:
(1)SSM + Shiro
(2)Spring Boot/Spring Cloud + Spring Security
以上隻是一個推薦的組合而已,如果單純從技術上來說,無論怎麼組合,都是可以運作的。
三、Spring Security過濾器
1、Spring Security中常見過濾器
- FilterSecurityInterceptor:是一個方法級的權限過濾器, 基本位于過濾鍊的最底部。
- ExceptionTranslationFilter:是個異常過濾器,用來處理在認證授權過程中抛出的異常。
- UsernamePasswordAuthenticationFilter :對/login 的 POST 請求做攔截,校驗表單中使用者
名,密碼。
2、15種過濾器
SpringSecurity 采用的是責任鍊的設計模式,它有一條很長的過濾器鍊。現在對這條過濾器鍊的 15 個過濾器進行說明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文與 Spring Web 中用于處理異步請求映射的 WebAsyncManager 進行內建。
(2) SecurityContextPersistenceFilter:在每次請求處理之前将該請求相關的安全上下文資訊加載到 SecurityContextHolder 中,然後在該次請求處理完成之後,将SecurityContextHolder 中關于這次請求的資訊存儲到一個“倉儲”中,然後将SecurityContextHolder 中的資訊清除,例如Session 中維護一個使用者的安全資訊就是這個過濾器處理的。
(3) HeaderWriterFilter:用于将頭資訊加入響應中。
(4) CsrfFilter:用于處理跨站請求僞造。
(5)LogoutFilter:用于處理登出。
(6)UsernamePasswordAuthenticationFilter:用于處理基于表單的登入請求,從表單中擷取使用者名和密碼。預設情況下處理來自 /login 的請求。從表單中擷取使用者名和密碼時,預設使用的表單 name 值為 username 和 password,這兩個值可以通過設定這個過濾器的 usernameParameter 和 passwordParameter 兩個參數的值進行修改。
(7)DefaultLoginPageGeneratingFilter:如果沒有配置登入頁面,那系統初始化時就會配置這個過濾器,并且用于在需要進行登入時生成一個登入表單頁面。
(8)BasicAuthenticationFilter:檢測和處理 http basic 認證。
(9)RequestCacheAwareFilter:用來處理請求的緩存。
(10)SecurityContextHolderAwareRequestFilter:主要是包裝請求對象 request。
(11)AnonymousAuthenticationFilter:檢測 SecurityContextHolder 中是否存在Authentication 對象,如果不存在為其提供一個匿名 Authentication。
(12)SessionManagementFilter:管理 session 的過濾器
(13)ExceptionTranslationFilter:處理 AccessDeniedException 和AuthenticationException 異常。
(14)FilterSecurityInterceptor:可以看做過濾器鍊的出口。
(15)RememberMeAuthenticationFilter:當使用者沒有登入而直接通路資源時, 從 cookie裡找出使用者的資訊, 如果 Spring Security 能夠識别出使用者提供的 remember me cookie,使用者将不必填寫使用者名和密碼, 而是直接登入進入系統,該過濾器預設不開啟。
3、SpringSecurity 基本流程
Spring Security 采取過濾鍊實作認證與授權,隻有目前過濾器通過,才能進入下一個過濾器:
綠色部分是認證過濾器,需要我們自己配置,可以配置多個認證過濾器。認證過濾器可以使用 Spring Security 提供的認證過濾器,也可以自定義過濾器(例如:短信驗證)。認證過濾器要在 configure(HttpSecurity http)方法中配置,沒有配置不生效。
下面會重點介紹以下三個過濾器: UsernamePasswordAuthenticationFilter 過濾器:該過濾器會攔截前端送出的 POST 方式的登入表單請求,并進行身份認證。ExceptionTranslationFilter 過濾器:該過濾器不需要我們配置,對于前端送出的請求會直接放行,捕獲後續抛出的異常并進行處理(例如:權限通路限制)。FilterSecurityInterceptor 過濾器:該過濾器是過濾器鍊的最後一個過濾器,根據資源權限配置來判斷目前請求是否有權限通路對應的資源。如果通路受限會抛出相關異常,并 由 ExceptionTranslationFilter 過濾器進行捕獲和處理。
4、SpringSecurity 認證流程
認證流程是在 UsernamePasswordAuthenticationFilter 過濾器中處理的,具體流程如下所示:
四、PasswordEncoder 接口
// 表示把參數按照特定的解析規則進行解析
String encode(CharSequence rawPassword);
// 表示驗證從存儲中擷取的編碼密碼與編碼後送出的原始密碼是否比對。如果密碼匹
配,則傳回 true;如果不比對,則傳回 false。第一個參數表示需要被解析的密碼。第二個
參數表示存儲的密碼。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密碼能夠再次進行解析且達到更安全的結果則傳回 true,否則傳回
false。預設傳回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
BCryptPasswordEncoder 是 Spring Security 官方推薦的密碼解析器,平時多使用這個解析器。 BCryptPasswordEncoder 是對 bcrypt 強散列方法的具體實作。是基于 Hash 算法實作的單向加密。可以通過 strength 控制加密強度,預設 10。
五、SpringBoot整合Spring Security入門
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.guor</groupId>
<artifactId>securityProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>securityProject</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok用來簡化實體類-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.properties
server.port=8111
#spring.security.user.name=root
#spring.security.user.password=root
#mysql資料庫連接配接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
3、SecurityConfig
package com.guor.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("zs").password(password).roles("admin");
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
package com.guor.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import javax.sql.DataSource;
@Configuration
public class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
//注入資料源
@Autowired
private DataSource dataSource;
//配置對象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userService(userService).passwordEncoder(password());
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//退出
http.logout().logoutUrl("/logout").
logoutSuccessUrl("/test/hello").permitAll();
//配置沒有權限通路跳轉自定義頁面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() //自定義自己編寫的登入頁面
.loginPage("/on.html") //登入頁面設定
.loginProcessingUrl("/user/login") //登入通路路徑
.defaultSuccessUrl("/success.html").permitAll() //登入成功之後,跳轉路徑
.failureUrl("/unauth.html")
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //設定哪些路徑可以直接通路,不需要認證
//目前登入使用者,隻有具有admins權限才可以通路這個路徑
//1 hasAuthority方法
// .antMatchers("/test/index").hasAuthority("admins")
//2 hasAnyAuthority方法
// .antMatchers("/test/index").hasAnyAuthority("admins,manager")
//3 hasRole方法 ROLE_sale
.antMatchers("/test/index").hasRole("sale")
.anyRequest().authenticated()
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//設定有效時長,機關秒
.userDetailsService(userService);
// .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// .and().csrf().disable(); //關閉csrf防護
}
}
4、啟動類
package com.guor.security;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@MapperScan("com.guor.security.mapper")
@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
public class SecurityProjectApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityProjectApplication.class, args);
}
}
5、User
package com.guor.security.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
public class User {
private Integer id;
private String username;
private String password;
}
6、UserService
package com.guor.security.service;
import com.guor.security.entity.User;
import com.guor.security.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//調用usersMapper方法,根據使用者名查詢資料庫
QueryWrapper<Users> wrapper = new QueryWrapper();
// where username=?
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
//判斷
if(user == null) {//資料庫沒有使用者名,認證失敗
throw new UsernameNotFoundException("使用者名不存在!");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
//從查詢資料庫傳回users對象,得到使用者名和密碼,傳回
return new User(user.getUsername(),
new BCryptPasswordEncoder().encode(user.getPassword()),auths);
}
}
7、UserMapper
package com.guor.security.mapper;
import com.guor.security.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
8、UserController
package com.guor.security.controller;
import com.guor.security.entity.User;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/test")
public class UserController {
@GetMapping("hello")
public String hello() {
return "hello security";
}
@GetMapping("index")
public String index() {
return "hello index";
}
@GetMapping("update")
//@Secured({"ROLE_sale","ROLE_manager"})
//@PreAuthorize("hasAnyAuthority('admins')")
@PostAuthorize("hasAnyAuthority('admins')")
public String update() {
System.out.println("update......");
return "hello update";
}
@GetMapping("getAll")
@PostAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.username == 'admin1'")
public List<Users> getAllUser(){
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(11,"admin1","6666"));
list.add(new Users(21,"admin2","888"));
System.out.println(list);
return list;
}
}
六、微服務認證與授權實作思路
1、如果是基于 Session,那麼 Spring-security 會對 cookie 裡的 sessionid 進行解析,找到伺服器存儲的 session 資訊,然後判斷目前使用者是否符合請求的要求。
2、如果是 token,則是解析出 token,然後将目前請求加入到 Spring-security 管理的權限資訊中去。
如果系統的子產品衆多,每個子產品都需要進行授權與認證,是以我們選擇基于 token 的形式進行授權與認證,使用者根據使用者名密碼認證成功,然後擷取目前使用者角色的一系列權限值,并以使用者名為 key,權限清單為 value 的形式存入 redis 緩存中,根據使用者名相關資訊生成 token 傳回,浏覽器将 token 記錄到 cookie 中,每次調用 api 接口都預設将 token 攜帶到 header 請求頭中,Spring-security 解析 header 頭擷取 token 資訊,解析 token 擷取目前使用者名,根據使用者名就可以從 redis中擷取權限清單,這樣 Spring-security 就能夠判斷目前請求是否有權限通路。
七、微服務代碼執行個體
1、父工程pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>common</module>
<module>infrastructure</module>
<module>service</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>acl_parent</artifactId>
<packaging>pom</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>acl_parent</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.0.5</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.7.0</swagger.version>
<jwt.version>0.7.0</jwt.version>
<fastjson.version>1.2.28</fastjson.version>
<gson.version>2.8.2</gson.version>
<json.version>20170516</json.version>
<cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus 持久層-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模闆引擎, Mybatis Plus 代碼生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、common子產品
common子產品pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>acl_parent</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_base</module>
<module>spring_security</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided </scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided </scope>
</dependency>
<!--lombok用來簡化實體類:需要安裝lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided </scope>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided </scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X內建redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
</project>
3、common子產品 -> SpringSecurity子子產品
(1)核心配置類
Spring Security 的核心配置就是繼承 WebSecurityConfigurerAdapter 并注解@EnableWebSecurity 的配置。這個配置指明了使用者名密碼的處理方式、請求路徑、登入登出控制等和安全相關的配置
package com.atguigu.security.config;
import com.atguigu.security.filter.TokenAuthFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import com.atguigu.security.security.DefaultPasswordEncoder;
import com.atguigu.security.security.TokenLogoutHandler;
import com.atguigu.security.security.TokenManager;
import com.atguigu.security.security.UnauthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
private UserDetailsService userDetailsService;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 配置設定
* @param http
* @throws Exception
*/
//設定退出的位址和token,redis操作位址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//沒有權限通路
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路徑
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
//調用userDetailsService和密碼處理
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
//不進行認證的路徑,可以直接通路
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
(2)實體類
package com.atguigu.security.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "使用者實體類")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密碼")
private String password;
@ApiModelProperty(value = "昵稱")
private String nickName;
@ApiModelProperty(value = "使用者頭像")
private String salt;
@ApiModelProperty(value = "使用者簽名")
private String token;
}
(3)過濾器
package com.atguigu.security.filter;
import com.atguigu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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.ArrayList;
import java.util.Collection;
import java.util.List;
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//擷取目前認證成功使用者權限資訊
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判斷如果有權限資訊,放到權限上下文中
if(authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//從header擷取token
String token = request.getHeader("token");
if(token != null) {
//從token擷取使用者名
String username = tokenManager.getUserInfoFromToken(token);
//從redis擷取對應權限清單
List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authority = new ArrayList<>();
for(String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}
}
package com.atguigu.security.filter;
import com.atguigu.security.entity.SecurityUser;
import com.atguigu.security.entity.User;
import com.atguigu.security.security.TokenManager;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
//1 擷取表單送出使用者名和密碼
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//擷取表單送出資料
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),
new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
//2 認證成功調用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//認證成功,得到認證成功之後使用者資訊
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根據使用者名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//把使用者名稱和使用者權限清單放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//傳回token
ResponseUtil.out(response, R.ok().data("token",token));
}
//3 認證失敗調用的方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
(4)security
package com.atguigu.security.security;
import com.atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int strength) {
}
//進行MD5加密
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}
//進行密碼比對
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}
package com.atguigu.security.security;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//退出處理器
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1 從header裡面擷取token
//2 token不為空,移除token,從redis删除token
String token = request.getHeader("token");
if(token != null) {
//移除
tokenManager.removeToken(token);
//從token擷取使用者名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}
package com.atguigu.security.security;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class TokenManager {
//token有效時長
private long tokenEcpiration = 24*60*60*1000;
//編碼秘鑰
private String tokenSignKey = "123456";
//1 使用jwt根據使用者名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
//2 根據token字元串得到使用者資訊
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}
//3 删除token
public void removeToken(String token) { }
}
package com.atguigu.security.security;
import com.atguigu.utils.utils.R;
import com.atguigu.utils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(httpServletResponse, R.error());
}
}
4、common子產品 -> service_base
(1)RedisConfig
package com.atguigu.utils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching //開啟緩存
@Configuration //配置類
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解決查詢緩存轉換異常的問題
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解決亂碼的問題),過期時間600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
(2)SwaggerConfig
package com.atguigu.utils;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration//配置類
@EnableSwagger2 //swagger注解
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
//.paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("網站-課程中心API文檔")
.description("本文檔描述了課程中心微服務接口定義")
.version("1.0")
.contact(new Contact("java", "http://atguigu.com", "[email protected]"))
.build();
}
}
(3)工具類
package com.atguigu.utils.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出錯!!+" + e);
}
}
public static void main(String[] args) {
System.out.println(MD5.encrypt("111111"));
}
}
package com.atguigu.utils.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ResponseUtil {
public static void out(HttpServletResponse response, R r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.atguigu.utils.utils;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
//統一傳回結果的類
@Data
public class R {
private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<String, Object>();
//把構造方法私有
private R() {}
//成功靜态方法
public static R ok() {
R r = new R();
r.setSuccess(true);
r.setCode(20000);
r.setMessage("成功");
return r;
}
//失敗靜态方法
public static R error() {
R r = new R();
r.setSuccess(false);
r.setCode(20001);
r.setMessage("失敗");
return r;
}
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}
5、gateway子產品
(1)pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>infrastructure</artifactId>
<groupId>com.atguigu</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!--服務調用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
(2)application.properties
# 端口号
server.port=8222
# 服務名
spring.application.name=service-gateway
# nacos服務位址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 使用服務發現路由
spring.cloud.gateway.discovery.locator.enabled=true
# 配置路由規則
spring.cloud.gateway.routes[0].id=service-acl
# 設定路由uri lb://注冊服務名稱
spring.cloud.gateway.routes[0].uri=lb://service-acl
# 具體路徑規則
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
(3)解決跨域
package com.atguigu.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
@Configuration
public class CorsConfig {
//解決跨域
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}
(4)啟動類
package com.atguigu.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
6、service子產品
由于字數限制,這裡的代碼就不貼了,如果需要代碼參考可以點選spring security學習筆記領取
(1)pom.xml
(2)application.properties
以菜單權限管理CRUD為例:
(3)Permission
(4)PermissionController
(5)PermissionService
(6)PermissionMapper
(7)PermissionMapper.xml
(8)PermissionHelper
(9)啟動類