目錄
大綱
一、自定義資源權限規則
二、自定義登錄界面
三、自定義登錄成功處理
四、顯示登錄失敗信息
五、自定義登錄失敗處理
六、注銷登錄
七、登錄用戶數據獲取
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
- 登錄表單 method 必須為
- 配置 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 跳轉
注意:如果之前請求路徑,會有優先跳轉之前請求路徑,可以傳入第二個參數進行修改`
- successForwardUrl ?默認使用 forward 跳轉
三、自定義登錄成功處理
有時候頁面跳轉并不能滿足我們,特別是在前后端分離開發中就不需要成功之后跳轉頁面。
只需要給前端返回一個 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;}//.....}
}
MODE THREADLOCAL
:這種存放策略是將 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特點是在哪個線程中存儲就要在哪個線程中讀取,這其實非常適合 web 應用,因為在默認情況下,一個請求無論經過多少 Filter 到達 Servlet,都是由一個線程來處理的。這也是 SecurityContextHolder 的默認存儲策略,這種存儲策略意味著如果在具體的業務處理代碼中,開啟了子線程,在子線程中去獲取登錄用戶數據,就會獲取不到。MODE INHERITABLETHREADLOCAL
:這種存儲模式適用于多線程環境,如果希望在子線程中也能夠獲取到登錄用戶數據,那么可以使用這種存儲模式。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
總結:
-
- 默認自動配置創建全局AuthenticationManager 默認找當前項目中是否存在自定義 UserDetailService 實例 自動將當前項目UserDetailService 實例設置為數據源
- 默認自動配置創建全局AuthenticationManager 在工廠中使用時直接在代碼中注入即可
- 自定義全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overridepublic void configure(AuthenticationManagerBuilder builder) {//builder ....}
}
-
- 自定義全局 AuthenticationManager
總結
-
- 一旦通過 configure 方法自定義 AuthenticationManager實現 就回將工廠中自動配置AuthenticationManager 進行覆蓋
- 一旦通過 configure 方法自定義 AuthenticationManager實現 需要在實現中指定認證數據源對象 UserDetaiService 實例
- 一旦通過 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);}
- 測試驗證