03認證原理自定義認證添加認證驗證碼

目錄

大綱

一、自定義資源權限規則

二、自定義登錄界面

三、自定義登錄成功處理

四、顯示登錄失敗信息

五、自定義登錄失敗處理

六、注銷登錄

七、登錄用戶數據獲取

1. SecurityContextHolder

2. SecurityContextHolderStrategy

3. 代碼中獲取認證之后用戶數據

4. 多線程情況下獲取用戶數據

5. 頁面上獲取用戶信息

八、自定義認證數據源

1. 認證流程分析

2. 三者關系

3. 配置全局 AuthenticationManager

4. 自定義內存數據源

5. 自定義數據庫數據源

九、添加認證驗證碼

1. 配置驗證碼

2. 傳統 web 開發

3. 前后端分離開發


大綱

  • 認證配置
  • 表單認證
  • 注銷登錄
  • 前后端分離認證
  • 添加驗證碼

一、自定義資源權限規則

  • /index ?公共資源
  • /hello .... 受保護資源 權限管理

在項目中添加如下配置就可以實現對資源權限規則設定:

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin();}
}

# 說明
- permitAll() 代表放行該資源,該資源為公共資源 無需認證和授權可以直接訪問
- anyRequest().authenticated() 代表所有請求,必須認證之后才能訪問
- formLogin() 代表開啟表單認證
## 注意: 放行資源必須放在所有認證請求之前!

二、自定義登錄界面

  • 引入模板依賴
<!--thymeleaf-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

  • 定義登錄頁面 controller
@Controller
public class LoginController {@RequestMapping("/login.html")public String login() {return "login";}
}

  • 在 templates 中定義登錄界面
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登錄</title>
</head>
<body>
<h1>用戶登錄</h1>
<form method="post" th:action="@{/doLogin}">用戶名:<input name="uname" type="text"/><br>密碼:<input name="passwd" type="password"/><br><input type="submit" value="登錄"/>
</form>
</body>
</html>


需要注意的是

    • 登錄表單 method 必須為 post,action 的請求路徑為 /doLogin
    • 用戶名的 name 屬性為 uname
    • 密碼的 name 屬性為 passwd
  • 配置 Spring Security 配置類
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/login.html").permitAll().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html").loginProcessingUrl("/doLogin").usernameParameter("uname").passwordParameter("passwd").successForwardUrl("/index") 		 //forward 跳轉           注意:不會跳轉到之前請求路徑//.defaultSuccessUrl("/index")   //redirect 重定向    注意:如果之前請求路徑,會有優先跳轉之前請求路徑.failureUrl("/login.html").and().csrf().disable();//這里先關閉 CSRF}
}
  • successForwardUrl 、defaultSuccessUrl 這兩個方法都可以實現成功之后跳轉
    • successForwardUrl ?默認使用 forward 跳轉
      注意:不會跳轉到之前請求路徑
    • defaultSuccessUrl ? 默認使用 redirect 跳轉
      注意:如果之前請求路徑,會有優先跳轉之前請求路徑,可以傳入第二個參數進行修改`

三、自定義登錄成功處理

有時候頁面跳轉并不能滿足我們,特別是在前后端分離開發中就不需要成功之后跳轉頁面。

只需要給前端返回一個 JSON 通知登錄成功還是失敗與否。

這個時候可以通過自定義 AuthenticationSucccessHandler 實現

public interface AuthenticationSuccessHandler {/*** Called when a user has been successfully authenticated.* @param request the request which caused the successful authentication* @param response the response* @param authentication the <tt>Authentication</tt> object which was created during* the authentication process.*/void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException;
}

根據接口的描述信息,也可以得知登錄成功會自動回調這個方法,進一步查看它的默認實現,你會發現

successForwardUrl、defaultSuccessUrl也是由它的子類實現的

  • 自定義 AuthenticationSuccessHandler 實現
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登錄成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
  • 配置 AuthenticationSuccessHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//.....successHandler(new MyAuthenticationSuccessHandler()).failureUrl("/login.html").and().csrf().disable();//這里先關閉 CSRF}
}

四、顯示登錄失敗信息

為了能更直觀在登錄頁面看到異常錯誤信息,可以在登錄頁面中直接獲取異常信息。

Spring Security 在登錄失敗之后會將異常信息存儲到 request 、session作用域中 key 為

SPRING_SECURITY_LAST_EXCEPTION 命名屬性中,源碼可以參考 SimpleUrlAuthenticationFailureHandler :

  • 顯示異常信息
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登錄</title>
</head>
<body>....<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
</body>
</html>
  • 配置
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//...and().formLogin()//....//.failureUrl("/login.html").failureForwardUrl("/login.html").and().csrf().disable();//這里先關閉 CSRF}
}
  • failureUrl、failureForwardUrl 關系類似于之前提到的 successForwardUrl 、defaultSuccessUrl 方法
    • failureUrl 失敗以后的重定向跳轉
    • failureForwardUrl 失敗以后的 forward 跳轉
      注意:因此獲取 request 中異常信息,這里只能使用failureForwardUrl

五、自定義登錄失敗處理

和自定義登錄成功處理一樣,Spring Security 同樣為前后端分離開發提供了登錄失敗的處理,這個類就是

AuthenticationFailureHandler,源碼為:

public interface AuthenticationFailureHandler {/*** Called when an authentication attempt fails.* @param request the request during which the authentication attempt occurred.* @param response the response.* @param exception the exception which was thrown to reject the authentication* request.*/void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException;}

根據接口的描述信息,也可以得知登錄失敗會自動回調這個方法,進一步查看它的默認實現,你會發現

failureUrl、failureForwardUrl也是由它的子類實現的。

  • 自定義 AuthenticationFailureHandler 實現
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登錄失敗: "+exception.getMessage());result.put("status", 500);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
  • 配置 AuthenticationFailureHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//...failureHandler(new MyAuthenticationFailureHandler()).and().csrf().disable();//這里先關閉 CSRF}
}

六、注銷登錄

Spring Security 中也提供了默認的注銷登錄配置,在開發時也可以按照自己需求對注銷進行個性化定制。

  • 開啟注銷登錄默認開啟
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//....and().logout().logoutUrl("/logout").invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/login.html").and().csrf().disable();//這里先關閉 CSRF}
}
    • 通過 logout() 方法開啟注銷配置
    • logoutUrl 指定退出登錄請求地址,默認是 GET 請求,路徑為 /logout
    • invalidateHttpSession 退出時是否是 session 失效,默認值為 true
    • clearAuthentication 退出時是否清除認證信息,默認值為 true
    • logoutSuccessUrl 退出登錄時跳轉地址
  • 配置多個注銷登錄請求
    如果項目中有需要,開發者還可以配置多個注銷登錄的請求,同時還可以指定請求的方法:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//....and().logout().logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),new AntPathRequestMatcher("/logout","GET"))).invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/login.html").and().csrf().disable();//這里先關閉 CSRF}
}
  • 前后端分離注銷登錄配置
    如果是前后端分離開發,注銷成功之后就不需要頁面跳轉了,只需要將注銷成功的信息返回前端即可,此時我們可以通過自定義 LogoutSuccessHandler ?實現來返回注銷之后信息:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "注銷成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//.....and().formLogin()//....and().logout()//.logoutUrl("/logout").logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),new AntPathRequestMatcher("/logout","GET"))).invalidateHttpSession(true).clearAuthentication(true)//.logoutSuccessUrl("/login.html").logoutSuccessHandler(new MyLogoutSuccessHandler()).and().csrf().disable();//這里先關閉 CSRF}
}

七、登錄用戶數據獲取

1. SecurityContextHolder

Spring Security 會將登錄用戶數據保存在 Session 中。但是,為了使用方便,Spring Security在此基

礎上還做了一些改進,其中最主要的一個變化就是線程綁定。當用戶登錄成功后,Spring Security 會將登錄

成功的用戶信息保存到 SecurityContextHolder 中。

SecurityContextHolder 中的數據保存默認是通過ThreadLocal 來實現的,使用 ThreadLocal 創建的

變量只能被當前線程訪問,不能被其他線程訪問和修改,也就是用戶數據和請求線程綁定在一起。當登錄

請求處理完畢后,Spring Security 會將 SecurityContextHolder中的數據拿出來保存到 Session 中,同時

將 SecurityContexHolder 中的數據清空。以后每當有請求到來時,Spring Security 就會先從Session 中

取出用戶登錄數據,保存到SecurityContextHolder 中,方便在該請求的后續處理過程中使用,同時在請

求結束時將SecurityContextHolder 中的數據拿出來保存到 Session 中,然后將SecurityContextHolder

中的數據清空。

實際上 SecurityContextHolder 中存儲是 SecurityContext,在 SecurityContext 中存儲是

Authentication。

這種設計是典型的策略設計模式:

public class SecurityContextHolder {public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";public static final String MODE_GLOBAL = "MODE_GLOBAL";private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";private static SecurityContextHolderStrategy strategy;//....private static void initializeStrategy() {if (MODE_PRE_INITIALIZED.equals(strategyName)) {Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED+ ", setContextHolderStrategy must be called with the fully constructed strategy");return;}if (!StringUtils.hasText(strategyName)) {// Set defaultstrategyName = MODE_THREADLOCAL;}if (strategyName.equals(MODE_THREADLOCAL)) {strategy = new ThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_GLOBAL)) {strategy = new GlobalSecurityContextHolderStrategy();return;}//.....}
}
  1. MODE THREADLOCAL:這種存放策略是將 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特點是在哪個線程中存儲就要在哪個線程中讀取,這其實非常適合 web 應用,因為在默認情況下,一個請求無論經過多少 Filter 到達 Servlet,都是由一個線程來處理的。這也是 SecurityContextHolder 的默認存儲策略,這種存儲策略意味著如果在具體的業務處理代碼中,開啟了子線程,在子線程中去獲取登錄用戶數據,就會獲取不到。
  2. MODE INHERITABLETHREADLOCAL:這種存儲模式適用于多線程環境,如果希望在子線程中也能夠獲取到登錄用戶數據,那么可以使用這種存儲模式。
  3. MODE GLOBAL:這種存儲模式實際上是將數據保存在一個靜態變量中,在 JavaWeb開發中,這種模式很少使用到。

2. SecurityContextHolderStrategy

通過 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接口用來定義存儲策略方法

public interface SecurityContextHolderStrategy {void clearContext();SecurityContext getContext();void setContext(SecurityContext context);SecurityContext createEmptyContext();
}

接口中一共定義了四個方法:

  • clearContext:該方法用來清除存儲的 SecurityContext對象。
  • getContext:該方法用來獲取存儲的 SecurityContext 對象。
  • setContext:該方法用來設置存儲的 SecurityContext 對象。
  • create Empty Context:該方法則用來創建一個空的 SecurityContext 對象。

從上面可以看出每一個實現類對應一種策略的實現。

3. 代碼中獲取認證之后用戶數據

@RestController
public class HelloController {@RequestMapping("/hello")public String hello() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User principal = (User) authentication.getPrincipal();System.out.println("身份 :"+principal.getUsername());System.out.println("憑證 :"+authentication.getCredentials());System.out.println("權限 :"+authentication.getAuthorities());return "hello security";}
}

4. 多線程情況下獲取用戶數據

@RestController
public class HelloController {@RequestMapping("/hello")public String hello() {new Thread(()->{Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User principal = (User) authentication.getPrincipal();System.out.println("身份 :"+principal.getUsername());System.out.println("憑證 :"+authentication.getCredentials());System.out.println("權限 :"+authentication.getAuthorities());}).start();return "hello security";}
}

可以看到默認策略,是無法在子線程中獲取用戶信息,如果需要在子線程中獲取必須使用第二種策略,默認策略

是通過 System.getProperty 加載的,因此我們可以通過增加 VM Options 參數進行修改。

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

5. 頁面上獲取用戶信息

  • 引入依賴
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version>
</dependency>
  • 頁面加入命名空間
<html lang="en" xmlns:th="https://www.thymeleaf.org" 
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  • 頁面中使用
<!--獲取認證用戶名-->
<ul><li sec:authentication="principal.username"></li><li sec:authentication="principal.authorities"></li><li sec:authentication="principal.accountNonExpired"></li><li sec:authentication="principal.accountNonLocked"></li><li sec:authentication="principal.credentialsNonExpired"></li>
</ul>

八、自定義認證數據源

1. 認證流程分析

Servlet Authentication Architecture :: Spring Security

  • 發起認證請求,請求中攜帶用戶名、密碼,該請求會被 UsernamePasswordAuthenticationFilter ?攔截
  • 在UsernamePasswordAuthenticationFilter的attemptAuthentication方法中將請求中用戶名和密碼,封裝為Authentication 對象,并交給AuthenticationManager 進行認證
  • 認證成功,將認證信息存儲到 SecurityContextHodler 以及調用記住我等,并回調AuthenticationSuccessHandler 處理
  • 認證失敗,清除 SecurityContextHodler 以及 記住我中信息,回調 AuthenticationFailureHandler 處理

2. 三者關系

從上面分析中得知,AuthenticationManager 是認證的核心類,但實際上在底層真正認證時還離不開

ProviderManager 以及AuthenticationProvider 。他們三者關系是樣的呢?

  • AuthenticationManager 是一個認證管理器,它定義了 Spring Security 過濾器要執行認證操作。
  • ProviderManager是AuthenticationManager接口的實現類。Spring Security 認證時默認使用就是 ProviderManager。
  • AuthenticationProvider 就是針對不同的身份類型執行的具體的身份認證。

AuthenticationManager 與 ProviderManager

ProviderManager 是 AuthenticationManager 的唯一實現,也是 Spring Security 默認使用實現。

從這里不難看出默認情況下

AuthenticationManager 就是一個ProviderManager。

ProviderManager 與 AuthenticationProvider

摘自官方: Servlet Authentication Architecture :: Spring Security

在 Spring Seourity 中,允許系統同時支持多種不同的認證方式,例如同時支持用戶名/密碼認證、

ReremberMe 認證、手機號碼動態認證等,而不同的認證方式對應了不同的 AuthenticationProvider,所

以一個完整的認證流程可能由多個 AuthenticationProvider 來提供。

多個 AuthenticationProvider 將組成一個列表,這個列表將由 ProviderManager 代理。換句話說,

在ProviderManager 中存在一個AuthenticationProvider 列表,在Provider Manager 中遍歷列表中的每

一個 AuthenticationProvider 去執行身份認證,最終得到認證結果。

ProviderManager 本身也可以再配置一個 AuthenticationManager 作為 parent,這樣當

ProviderManager 認證失敗之后,就可以進入到 parent 中再次進行認證。

理論上來說,ProviderManager 的 parent 可以是任意類型的 AuthenticationManager,但是通常都

是由ProviderManager 來扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的

parent。

ProviderManager 本身也可以有多個,多個ProviderManager 共用同一個 parent。有時,一個應用

程序有受保護資源的邏輯組(例如,所有符合路徑模式的網絡資源,如/api/**),每個組可以有自己的專

用 AuthenticationManager。

通常,每個組都是一個ProviderManager,它們共享一個父級。然后,父級是一種 全局資源,作為

所有提供者的后備資源。

根據上面的介紹,我們繪出新的 AuthenticationManager、ProvideManager 和

AuthentictionProvider 關系

摘自官網: Architecture :: Spring Security

弄清楚認證原理之后我們來看下具體認證時數據源的獲取。

默認情況下 AuthenticationProvider 是由 DaoAuthenticationProvider 類來實現認證的,

DaoAuthenticationProvider 認證時又通過 UserDetailsService 完成數據源的校驗。他們之

間調用關系如下:

總結:AuthenticationManager 是認證管理器,在 Spring Security 中有全局AuthenticationManager,也

可以有局部AuthenticationManager。全局的AuthenticationManager用來對全局認證進行處理,局部的

AuthenticationManager用來對某些特殊資源認證處理。當然無論是全局認證管理器還是局部認證管理器都是

由 ProviderManger 進行實現。 每一個ProviderManger中都代理一個AuthenticationProvider的列表,列

表中每一個實現代表一種身份認證方式。認證時底層數據源需要調用 UserDetailService 來實現。

3. 配置全局 AuthenticationManager

Architecture :: Spring Security

  • 默認的全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredpublic void initialize(AuthenticationManagerBuilder builder) {//builder..}
}
    • springboot 對 security 進行自動配置時自動在工廠中創建一個全局AuthenticationManager

總結:

    1. 默認自動配置創建全局AuthenticationManager 默認找當前項目中是否存在自定義 UserDetailService 實例 自動將當前項目UserDetailService 實例設置為數據源
    2. 默認自動配置創建全局AuthenticationManager 在工廠中使用時直接在代碼中注入即可
  • 自定義全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overridepublic void configure(AuthenticationManagerBuilder builder) {//builder ....}
}
    • 自定義全局 AuthenticationManager

總結

    1. 一旦通過 configure 方法自定義 AuthenticationManager實現 就回將工廠中自動配置AuthenticationManager 進行覆蓋
    2. 一旦通過 configure 方法自定義 AuthenticationManager實現 需要在實現中指定認證數據源對象 UserDetaiService 實例
    3. 一旦通過 configure 方法自定義 AuthenticationManager實現 這種方式創建AuthenticationManager對象工廠內部本地一個AuthenticationManager 對象 不允許在其他自定義組件中進行注入
  • 用來在工廠中暴露自定義AuthenticationManager 實例
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {//1.自定義AuthenticationManager  推薦  并沒有在工廠中暴露出來@Overridepublic void configure(AuthenticationManagerBuilder builder) throws Exception {System.out.println("自定義AuthenticationManager: " + builder);builder.userDetailsService(userDetailsService());}//作用: 用來將自定義AuthenticationManager在工廠中進行暴露,可以在任何位置注入@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

4. 自定義內存數據源

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager= new InMemoryUserDetailsManager();UserDetails u1 = User.withUsername("zhangs").password("{noop}111").roles("USER").build();inMemoryUserDetailsManager.createUser(u1);return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}  	
}

5. 自定義數據庫數據源

  • 設計表結構
-- 用戶表
CREATE TABLE `user`
(`id`                    int(11) NOT NULL AUTO_INCREMENT,`username`              varchar(32)  DEFAULT NULL,`password`              varchar(255) DEFAULT NULL,`enabled`               tinyint(1) DEFAULT NULL,`accountNonExpired`     tinyint(1) DEFAULT NULL,`accountNonLocked`      tinyint(1) DEFAULT NULL,`credentialsNonExpired` tinyint(1) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(`id`      int(11) NOT NULL AUTO_INCREMENT,`name`    varchar(32) DEFAULT NULL,`name_zh` varchar(32) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用戶角色關系表
CREATE TABLE `user_role`
(`id`  int(11) NOT NULL AUTO_INCREMENT,`uid` int(11) DEFAULT NULL,`rid` int(11) DEFAULT NULL,PRIMARY KEY (`id`),KEY   `uid` (`uid`),KEY   `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
  • 插入測試數據
-- 插入用戶數據
BEGIN;INSERT INTO `user`VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
COMMIT;
-- 插入角色數據
BEGIN;INSERT INTO `role`VALUES (1, 'ROLE_product', '商品管理員');INSERT INTO `role`VALUES (2, 'ROLE_admin', '系統管理員');INSERT INTO `role`VALUES (3, 'ROLE_user', '用戶管理員');
COMMIT;
-- 插入用戶角色數據
BEGIN;INSERT INTO `user_role`VALUES (1, 1, 1);INSERT INTO `user_role`VALUES (2, 1, 2);INSERT INTO `user_role`VALUES (3, 2, 2);INSERT INTO `user_role`VALUES (4, 3, 3);
COMMIT;
  • 項目中引入依賴
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.38</version>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.7</version>
</dependency>
  • 配置 springboot 配置文件
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root# mybatis
mybatis.mapper-locations=classpath:com/baizhi/mapper/*.xml
mybatis.type-aliases-package=com.baizhi.entity# log
logging.level.com.baizhi=debug

  • 創建 entity
    • 創建 user 對象
public class User implements UserDetails {private Integer id;private String username;private String password;private Boolean enabled;private Boolean accountNonExpired;private Boolean accountNonLocked;private Boolean credentialsNonExpired;private List<Role> roles = new ArrayList<>();@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));return grantedAuthorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic boolean isEnabled() {return enabled;}//get/set....
}
    • 創建 role 對象
public class Role {private Integer id;private String name;private String nameZh;//get set..
}
  • 創建 UserDao 接口
@Mapper
public interface UserDao {//根據用戶名查詢用戶User loadUserByUsername(String username);//根據用戶id查詢角色List<Role> getRolesByUid(Integer uid);
}
  • 創建 UserMapper 實現
<?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.baizhi.dao.UserDao"><!--查詢單個--><select id="loadUserByUsername" resultType="User">select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpiredfrom userwhere username = #{username}</select><!--查詢指定行數據--><select id="getRolesByUid" resultType="Role">select r.id,r.name,r.name_zh nameZhfrom role r,user_role urwhere r.id = ur.ridand ur.uid = #{uid}</select>
</mapper>
  • 創建 UserDetailService 實例
@Component
public class MyUserDetailService implements UserDetailsService {private  final UserDao userDao;@Autowiredpublic MyUserDetailService(UserDao userDao) {this.userDao = userDao;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDao.loadUserByUsername(username);if(ObjectUtils.isEmpty(user))throw new RuntimeException("用戶不存在");user.setRoles(userDao.getRolesByUid(user.getId()));return user;}
}
  • 配置 authenticationManager 使用自定義UserDetailService
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {private final UserDetailsService userDetailsService;@Autowiredpublic WebSecurityConfigurer(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overrideprotected void configure(AuthenticationManagerBuilder builder) throws Exception {builder.userDetailsService(userDetailsService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {//web security..}
}
  • 啟動測試即可

九、添加認證驗證碼

1. 配置驗證碼

<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version>
</dependency>
@Configuration
public class KaptchaConfig {@Beanpublic Producer kaptcha() {Properties properties = new Properties();properties.setProperty("kaptcha.image.width", "150");properties.setProperty("kaptcha.image.height", "50");properties.setProperty("kaptcha.textproducer.char.string", "0123456789");properties.setProperty("kaptcha.textproducer.char.length", "4");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}

2. 傳統 web 開發

  • 生成驗證碼 controller
@Controller
public class KaptchaController {private final Producer producer;@Autowiredpublic KaptchaController(Producer producer) {this.producer = producer;}@GetMapping("/vc.jpg")public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {response.setContentType("image/png");String code = producer.createText();session.setAttribute("kaptcha", code);//可以更換成 redis 實現BufferedImage bi = producer.createImage(code);ServletOutputStream os = response.getOutputStream();ImageIO.write(bi, "jpg", os);}
}
  • 自定義驗證碼異常類
public class KaptchaNotMatchException extends AuthenticationException {public KaptchaNotMatchException(String msg) {super(msg);}public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}
}
  • 自定義filter驗證驗證碼
public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {public static final String KAPTCHA_KEY = "kaptcha";//默認值private String kaptcha = KAPTCHA_KEY;public String getKaptcha() {return kaptcha;}public void setKaptcha(String kaptcha) {this.kaptcha = kaptcha;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//1.判斷是否是 post 方式if (request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//2.獲取驗證碼String kaptcha = request.getParameter(getKaptcha());String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionKaptcha) &&kaptcha.equalsIgnoreCase(sessionKaptcha)) {return super.attemptAuthentication(request, response);}throw new KaptchaNotMatchException("驗證碼輸入錯誤!");}
}
  • 放行以及配置驗證碼 filter
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {private final UserDetailsService userDetailsService;@Autowiredpublic WebSecurityConfigurer(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overrideprotected void configure(AuthenticationManagerBuilder builder) throws Exception {builder.userDetailsService(userDetailsService);}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic KaptchaFilter kaptchaFilter() throws Exception {KaptchaFilter kaptchaFilter = new KaptchaFilter();//指定接收驗證碼請求參數名kaptchaFilter.setKaptcha("kaptcha");//指定認證管理器kaptchaFilter.setAuthenticationManager(authenticationManagerBean());//指定認證成功和失敗處理kaptchaFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());kaptchaFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());//指定處理登錄kaptchaFilter.setFilterProcessesUrl("/doLogin");kaptchaFilter.setUsernameParameter("uname");kaptchaFilter.setPasswordParameter("passwd");return kaptchaFilter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/vc.jpg").permitAll().mvcMatchers("/login.html").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login.html")...http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);}
}
  • 登錄頁面添加驗證碼
<form method="post" th:action="@{/doLogin}">用戶名:<input name="uname" type="text"/><br>密碼:<input name="passwd" type="password"/><br>驗證碼: <input name="kaptcha" type="text"/> <img alt="" th:src="@{/vc.jpg}"><br><input type="submit" value="登錄"/>
</form>

3. 前后端分離開發

  • 生成驗證碼 controller
@RestController
public class KaptchaController {private final Producer producer;@Autowiredpublic KaptchaController(Producer producer) {this.producer = producer;}@GetMapping("/vc.png")public String getVerifyCode(HttpSession session) throws IOException {//1.生成驗證碼String code = producer.createText();session.setAttribute("kaptcha", code);//可以更換成 redis 實現BufferedImage bi = producer.createImage(code);//2.寫入內存FastByteArrayOutputStream fos = new FastByteArrayOutputStream();ImageIO.write(bi, "png", fos);//3.生成 base64return Base64.encodeBase64String(fos.toByteArray());}
}
  • 定義驗證碼異常類
public class KaptchaNotMatchException extends AuthenticationException {public KaptchaNotMatchException(String msg) {super(msg);}public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}
}
  • 在自定義LoginKaptchaFilter中加入驗證碼驗證
//自定義 filter
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {public static final String FORM_KAPTCHA_KEY = "kaptcha";private String kaptchaParameter = FORM_KAPTCHA_KEY;public String getKaptchaParameter() {return kaptchaParameter;}public void setKaptchaParameter(String kaptchaParameter) {this.kaptchaParameter = kaptchaParameter;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}try {//1.獲取請求數據Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String kaptcha = userInfo.get(getKaptchaParameter());//用來獲取數據中驗證碼String username = userInfo.get(getUsernameParameter());//用來接收用戶名String password = userInfo.get(getPasswordParameter());//用來接收密碼//2.獲取 session 中驗證碼String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha");if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&kaptcha.equalsIgnoreCase(sessionVerifyCode)) {//3.獲取用戶名 和密碼認證UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}} catch (IOException e) {e.printStackTrace();}throw new KaptchaNotMatchException("驗證碼不匹配!");}
}
  • 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//自定義內存數據源@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());return inMemoryUserDetailsManager;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//配置@Beanpublic LoginKaptchaFilter loginKaptchaFilter() throws Exception {LoginKaptchaFilter loginKaptchaFilter = new LoginKaptchaFilter();//1.認證 urlloginKaptchaFilter.setFilterProcessesUrl("/doLogin");//2.認證 接收參數loginKaptchaFilter.setUsernameParameter("uname");loginKaptchaFilter.setPasswordParameter("passwd");loginKaptchaFilter.setKaptchaParameter("kaptcha");//3.指定認證管理器loginKaptchaFilter.setAuthenticationManager(authenticationManagerBean());//4.指定成功時處理loginKaptchaFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登錄成功");result.put("用戶信息", authentication.getPrincipal());resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.OK.value());String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});//5.認證失敗處理loginKaptchaFilter.setAuthenticationFailureHandler((req, resp, ex) -> {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登錄失敗: " + ex.getMessage());resp.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());resp.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);resp.getWriter().println(s);});return loginKaptchaFilter;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().mvcMatchers("/vc.jpg").permitAll().anyRequest().authenticated().and().formLogin().and().exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {resp.setContentType("application/json;charset=UTF-8");resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("必須認證之后才能訪問!");}).and().logout().and().csrf().disable();http.addFilterAt(loginKaptchaFilter(), UsernamePasswordAuthenticationFilter.class);}
  • 測試驗證

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/86989.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/86989.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/86989.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

IPLOOK 2025上半年足跡回顧:連接全球,步履不停

2025年上半年&#xff0c;IPLOOK積極活躍于全球通信舞臺&#xff0c;足跡橫跨亞洲、歐洲、非洲與北美洲&#xff0c;我們圍繞5G核心網、私有網絡、云化架構等方向&#xff0c;向來自不同地區的客戶與合作伙伴展示了領先的端到端解決方案&#xff0c;深入了解各地市場需求與技術…

【Kafka】docker 中配置帶 Kerberos 認證的 Kafka 環境(全過程)

1. 準備 docker 下載鏡像 docker pull centos/systemd&#xff0c;該鏡像是基于 centos7 增加了 systemd 的功能&#xff0c;可以更方便啟動后臺服務 啟動鏡像 使用 systemd 功能需要權限&#xff0c;如果是模擬 gitlab services 就不要使用 systemd 的方式啟動 如果不使用 s…

用Python構建一個可擴展的多網盤聚合管理工具 (以阿里云盤為例)

摘要 本文旨在從開發者視角&#xff0c;探討并實踐如何構建一個命令行界面的、支持多網盤聚合管理的工具。我們將以阿里云盤為例&#xff0c;深入解析其API認證與核心操作&#xff0c;并用Python從零開始實現文件列表、重命名、分享等功能。更重要的是&#xff0c;本文將重點討…

筑牢網絡安全屏障

在數字化浪潮席卷全球的今天&#xff0c;網絡空間已成為繼陸、海、空、天之后的 “第五疆域”&#xff0c;深刻影響著國家政治、經濟、軍事等各個領域。“沒有網絡安全就沒有國家安全”&#xff0c;這句論斷精準道出了網絡安全與國家安全之間密不可分的關系。? 網絡安全關乎國…

計算機網絡(一)層

一、分層 分層的意義&#xff1a;簡化復雜性、提高靈活性、促進標準化 &#xff08;1&#xff09;法律上國際標準——OSI體系結構 &#xff08;2&#xff09;事實上的網絡標準——TCP/IP體系結構 TCP&#xff1a;運輸層的協議 IP&#xff1a;網際層的一個協議 網絡接口層&…

STM32 rs485實現中斷DMA模式收發不定長數據

在STM32F103上使用TD301D485H模塊通過USB轉485/422串口線與電腦通信 TXD (TD301D485H) -> PA2 (STM32F103)RXD (TD301D485H) -> PA3 (STM32F103)CON (TD301D485H) -> PA1 (STM32F103) 由于485是半雙工通信&#xff0c;需要在發送和接收時控制方向引腳&#xff08;CO…

DDL-8-小結

DDL 小結 DDL 小結 DDL 小結DDL - 數據庫操作DDL - 表操作 DDL - 數據庫操作 查看當前有哪些數據庫 SHOW DATABASES;新建數據庫 CREATE DATABASE 數據庫名;使用數據庫 USE 數據庫名;查詢當前數據庫 SELECT DATABASE();刪除數據庫 DROP DATABASE 數據庫名;DDL - 表操作 查看當前…

Redis 安裝使用教程

一、Redis 簡介 Redis 是一個開源&#xff08;BSD 許可&#xff09;、內存數據結構存儲系統&#xff0c;可以用作數據庫、緩存和消息中間件。支持字符串、哈希、列表、集合、有序集合等數據類型&#xff0c;廣泛應用于分布式緩存、排行榜、實時數據分析等場景。 二、下載安裝…

Go語言測試與調試:單元測試與基準測試

以下是《Go語言實戰指南》中關于 測試與調試&#xff1a;單元測試與基準測試 的詳細內容&#xff0c;涵蓋測試編寫、運行、覆蓋率分析與性能測試&#xff0c;適用于實際項目開發與性能優化階段。 一、Go 的測試體系概覽 Go 提供原生的測試工具包 testing&#xff0c;無需第三方…

數字FIR-I型濾波器設計(窗函數法)

目錄 一、實驗目的 二、實驗原理 2.1 概念辨析 2.2 代碼實現邏輯與工具函數 三、實驗內容 3.1 設計帶通濾波器&#xff08;電平組合法&#xff0c;&#xff08;理想寬帶低通-理想窄帶低通&#xff09;x窗函數&#xff09; 3.2 高通濾波器&#xff08;…

RHCSA認證題目練習一(配置網絡設置)

一. 題目 配置網絡設置 解題過程&#xff1a; 注意&#xff1a;不可以在xshell中完成&#xff0c;否則會直接斷聯 這里用圖形化解題&#xff0c;更加簡單防止命令記錯 1. 打開圖形化視圖 命令&#xff1a;nmtui 按回車確認 按回車確認 2.首先把IPv4配置 <自動> 改成 …

STL簡介+string模擬實現

STL簡介string模擬實現 1. 什么是STL2. STL的版本3. STL的六大組件4.STL的缺陷5. string5.1 C語言中的字符串5.2 1個OJ題 6.標準庫中的string類6.1 string類(了解)6.2 string類的常用接口說明1.string類對象的常見構造函數2.析構函數(~string())3.賦值函數 (operator) 6.3 stri…

golang實現一個mysql中隨機獲取cookies的API

之前用FASTAPI寫了一個隨機cookies請求的接口,現在嘗試用golang實現同樣的效果 1.編寫go代碼 package mainimport ("database/sql""encoding/json""fmt"_ "github.com/go-sql-driver/mysql""log""net/http"&quo…

[Vue2組件]三角形角標

[Vue2組件]三角形角標 <template><div class"ys-subscript" :style"svgStyle"><svg width"200" height"200" viewBox"0 0 200 200" xmlns"http://www.w3.org/2000/svg"><!-- 三角形背景 - 右…

洛谷刷題4

B4354 [GESP202506 一級] 假期閱讀 題目傳送門 B4354 難度&#xff1a;入門 很簡單的題&#xff0c;如果小A看的頁數≤這本書的頁數&#xff0c;輸出他看的頁數 否則&#xff0c;輸出這本書的頁數 AC代碼&#xff1a; #include <iostream> using namespace std; in…

【基于Echarts的地圖可視化】

<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>中國牛只分布可視化</title><script src"https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js"></script><script src"h…

系統架構設計師備考之架構設計基礎

1.計算機系統基礎知識 1.1.計算機系統概述 計算機系統的定義與組成 計算機系統是指用于數據管理的計算機硬件、軟件及網絡組成的系統。 計算機系統可劃分為硬件和軟件兩部分。硬件由機械、電子元器件、磁介質和光介質等物理實體構成&#xff1b; 軟件是一系列按照特定順序組織…

針對華為云服務器使用率過大

從這兩張監控圖可以看出&#xff0c;服務器在大約上午 10:30 前后經歷了一次明顯的負載變化&#xff1a; 1. 圖表解讀 CPU 使用率 從凌晨到上午約 10:00 前&#xff0c;CPU 基本處于 0–2% 的閑置狀態。10:00–14:00 之間&#xff0c;CPU 利用率逐步攀升&#xff0c;多次沖擊 3…

記dwz(JUI)前端框架使用之--服務端響應提示框

目錄 前言 一、DWZ服務器端響應種類 二、如何增加info級別的消息提示 1.打開項目的BaseController.java類 2.打開項目的dwz.min.js文件 3.最后在前端DWZ的主加載頁面或者js文件中添加如下代碼&#xff1a; 前言 本篇文章沒有講太多東西&#xff0c;主要是個人工作記錄保…

leetcode 295. 數據流的中位數

時間復雜度分析&#xff1a;為什么你的中位數查找方案會超時&#xff1f; 分析你提供的MedianFinder實現&#xff0c;其時間復雜度較高的原因主要在于findMedian函數的實現方式。讓我詳細解釋&#xff1a; 代碼時間復雜度分析 你的代碼中兩個主要函數的時間復雜度如下&#…