SpringBoot應用篇(二):SpringSecurity實現帶驗證碼的登錄認證 附代碼


一、文章簡介

本文簡要介紹了spring security的基本原理和實現,並基於springboot整合了spring security實現了基於數據庫管理的用戶的登錄和登出,登錄過程實現了驗證碼的校驗功能。

完整代碼地址:https://github.com/hello-shf/spring-security.git

二、spring security框架簡介

  Spring Security是一個能夠為基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。主要包括:用戶認證(Authentication)和用戶授權(Authorization)兩個部分。用戶認證指的是驗證某個用戶能否訪問該系統。用戶認證過程一般要求用戶提供用戶名和密碼。系統通過校驗用戶名和密碼來完成認證過程。用戶授權指的是驗證某個用戶是否有權限執行某個操作或訪問某個頁面。通常在一個企業級的系統中不同的用戶所具有的權限也是不同的,簡單的來說比如普通用戶和管理員的區別,管理員顯然具有更高的權限。一般來說,系統會為不同的用戶分配不同的角色,而每個角色則對應一系列的權限。spring security的主要核心功能為認證和授權,所有的架構也是基於這兩個核心功能去實現的。

三、spring security原理

  Spring security提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI,和AOP功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。Spring Security對Web安全性的支持大量地依賴於Servlet過濾器。這些過濾器攔截進入請求,並且在應用程序處理該請求之前進行某些安全處理。 Spring Security提供有若干個過濾器,它們能夠攔截Servlet請求,並將這些請求轉給認證和訪問決策管理器處理,從而增強安全性。

 四、spring boot整合spring security

4.1 准備工作

4.1.1數據庫

 1 DROP TABLE IF EXISTS `t_user`;
 2 CREATE TABLE `t_user`  (
 3   `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 4   `code` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用戶編碼',
 5   `create_time` timestamp(0) NOT NULL DEFAULT '2019-01-01 00:00:00' COMMENT '注冊時間',
 6   `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新時間',
 7   `is_delete` int(1) NOT NULL DEFAULT 0 COMMENT '是否刪除 0:未刪除 1:刪除',
 8   `username` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶名',
 9   `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
10   `role` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶角色',
11   `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '手機號',
12   `email` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '郵箱',
13   PRIMARY KEY (`id`) USING BTREE,
14   UNIQUE INDEX `username`(`username`) USING BTREE
15 ) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用戶表' ROW_FORMAT = Compact;
16 INSERT INTO `t_user` VALUES (1, 'ef269d06e6b1497fbb209becca248251', '2019-04-22 14:24:10', '2019-04-29 06:55:39', 0, '學友', 'admin1', '1', '18888888888', '8888@qq.com');
17 INSERT INTO `t_user` VALUES (2, '074aca14664b49ce9165bc597d928078', '2019-01-01 00:00:00', '2019-05-01 18:10:54', 0, '德華', 'admin', '1', '18839339393', '8888@qq.com');
18 INSERT INTO `t_user` VALUES (3, '0bad7a4fea5f4c129c454cdf658744ec', '2019-01-01 00:00:00', '2019-05-01 18:11:13', 0, '富城', 'admin', '1', '18839339393', '8888@qq.com');
View Code

 

4.1.2 pom.xml依賴

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4     <modelVersion>4.0.0</modelVersion>
 5     <parent>
 6         <groupId>org.springframework.boot</groupId>
 7         <artifactId>spring-boot-starter-parent</artifactId>
 8         <version>1.5.10.RELEASE</version>
 9         <relativePath/> <!-- lookup parent from repository -->
10     </parent>
11     <groupId>com.shf</groupId>
12     <artifactId>sping-boot-security</artifactId>
13     <version>0.0.1-SNAPSHOT</version>
14     <name>sping-boot-security</name>
15     <description>Demo project for Spring Boot</description>
16 
17     <properties>
18         <java.version>1.8</java.version>
19     </properties>
20 
21     <dependencies>
22         <dependency>
23             <groupId>org.projectlombok</groupId>
24             <artifactId>lombok</artifactId>
25         </dependency>
26 
27         <dependency>
28             <groupId>org.springframework.boot</groupId>
29             <artifactId>spring-boot-starter-security</artifactId>
30             <version>1.5.10.RELEASE</version>
31         </dependency>
32 
33         <dependency>
34             <groupId>mysql</groupId>
35             <artifactId>mysql-connector-java</artifactId>
36         </dependency>
37 
38         <dependency>
39             <groupId>org.springframework.boot</groupId>
40             <artifactId>spring-boot-starter-web</artifactId>
41         </dependency>
42 
43         <dependency>
44             <groupId>org.springframework.boot</groupId>
45             <artifactId>spring-boot-starter-data-jpa</artifactId>
46         </dependency>
47     </dependencies>
48 
49     <build>
50         <plugins>
51             <plugin>
52                 <groupId>org.springframework.boot</groupId>
53                 <artifactId>spring-boot-maven-plugin</artifactId>
54             </plugin>
55         </plugins>
56     </build>
57 
58 </project>
View Code

4.1.3  application.properties

1 spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=GMT
2 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
3 spring.datasource.username=root
4 spring.datasource.password=

 

4.2 代碼實現

4.2.1 t_user表的實體類TUser的基本操作

實體類的基本增刪改查可依據項目需要自行選擇合適的ORM框架,此處我采用的是jpa實現的基本用戶查詢操作。此模塊不在過多贅述,直接上代碼

TUser.java實體類

 1 package com.shf.security.user.entity;
 2 
 3 import lombok.Data;
 4 
 5 import javax.persistence.Entity;
 6 import javax.persistence.Id;
 7 import javax.persistence.Table;
 8 import java.util.Date;
 9 
10 /**
11  * 描述:用戶表實體
12  * @author: shf
13  * @date: 2019-04-19 16:24:04
14  * @version: V1.0
15  */
16 @Data
17 @Entity
18 @Table(name = "t_user")
19 public class TUser {
20     /**
21      * 主鍵
22      */
23     @Id
24     private Integer id;
25     /**
26      * 用戶編碼
27      */
28 
29     private String code;
30     /**
31      * 注冊時間
32      */
33     private Date createTime;
34     /**
35      * 更新時間
36      */
37     private Date updateTime;
38     /**
39      * 是否刪除 0:刪除 1:未刪除
40      */
41     private Integer isDelete;
42     /**
43      * 用戶名
44      */
45     private String username;
46     /**
47      * 密碼
48      */
49     private String password;
50     /**
51      * 用戶角色
52      */
53     private String role;
54     /**
55      * 手機號
56      */
57     private String phone;
58     /**
59      * 郵箱
60      */
61     private String email;
62 }
View Code

 

TUserDao.java類

 1 package com.shf.security.user.dao;
 2 
 3 import com.shf.security.user.entity.TUser;
 4 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 5 import org.springframework.data.jpa.repository.Query;
 6 import org.springframework.data.repository.CrudRepository;
 7 
 8 public interface TUserDao extends CrudRepository<TUser, Long>, JpaSpecificationExecutor<TUser> {
 9 
10     @Query("select t from TUser t where t.username=?1")
11     public TUser findByName(String username);
12 }
View Code

 

TUserService.java接口

 1 package com.shf.security.user.service;
 2 
 3 import com.shf.security.user.entity.TUser;
 4 
 5 /**
 6  * 描述:用戶表服務類
 7  * @author: shf
 8  * @date: 2019-04-19 16:24:04
 9  * @version: V1.0
10  */
11 public interface TUserService{
12     /**
13      * @param username
14      * @return
15      */
16     public TUser findByName(String username);
17 }
View Code

 

TUserServiceImpl.java

 1 package com.shf.security.user.service.impl;
 2 
 3 import com.shf.security.user.dao.TUserDao;
 4 import com.shf.security.user.entity.TUser;
 5 import com.shf.security.user.service.TUserService;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.stereotype.Service;
 8 
 9 
10 /**
11  * 描述:
12  * @author: shf
13  * @date: 2017/11/16 0016 13:12
14  * @version: V1.0
15  */
16 @Service
17 public class TUserServiceImpl implements TUserService {
18     @Autowired
19     private TUserDao userDao;
20 
21     @Override
22     public TUser findByName(String username) {
23         return userDao.findByName(username);
24     }
25 }
View Code

 4.2.2 生成驗證碼的工具

驗證碼生產工具VerifyCodeUtil.java

  1 package com.shf.security.utils;
  2 
  3 import javax.servlet.http.HttpSession;
  4 import java.awt.*;
  5 import java.awt.image.BufferedImage;
  6 import java.util.*;
  7 
  8 /**
  9  * 描述:
 10  *
 11  * @Author shf
 12  * @Description TODO
 13  * @Date 2019/5/5 10:53
 14  * @Version V1.0
 15  **/
 16 public class VerifyCodeUtil {
 17     public static final String SESSION_KEY = "verifyCode";
 18     public static final String BUFFIMG_KEY = "buffImg";
 19     /**
 20      * 驗證碼圖片的寬度。
 21      */
 22     private static int width = 100;
 23     public static final long VERIFYCODE_TIMEOUT = 30*1000;//一分鍾
 24 
 25     /**
 26      *  驗證碼圖片的高度。
 27      */
 28     private static int height = 30;
 29 
 30     /**
 31      * 驗證碼字符個數
 32      */
 33     private static int codeCount = 4;
 34 
 35     /**
 36      * 字體高度
 37      */
 38     private static int fontHeight;
 39 
 40     /**
 41      * 干擾線數量
 42      */
 43     private static int interLine = 12;
 44 
 45     /**
 46      * 第一個字符的x軸值,因為后面的字符坐標依次遞增,所以它們的x軸值是codeX的倍數
 47      */
 48     private static int codeX;
 49 
 50     /**
 51      * codeY ,驗證字符的y軸值,因為並行所以值一樣
 52      */
 53     private static int codeY;
 54 
 55     /**
 56      * codeSequence 表示字符允許出現的序列值
 57      */
 58     static char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
 59             'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
 60             'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
 61     public static Map<String, Object> getVerifyCode(){
 62         Map<String, Object> result = new HashMap<>();
 63         //width-4 除去左右多余的位置,使驗證碼更加集中顯示,減得越多越集中。
 64         //codeCount+1     //等比分配顯示的寬度,包括左右兩邊的空格
 65         codeX = (width-4) / (codeCount+1);
 66         //height - 10 集中顯示驗證碼
 67         fontHeight = height - 10;
 68         codeY = height - 7;
 69         // 定義圖像buffer
 70         BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
 71         Graphics2D gd = buffImg.createGraphics();
 72         // 創建一個隨機數生成器類
 73         Random random = new Random();
 74         // 將圖像填充為白色
 75         gd.setColor(Color.WHITE);
 76         gd.fillRect(0, 0, width, height);
 77         // 創建字體,字體的大小應該根據圖片的高度來定。
 78         Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
 79         // 設置字體。
 80         gd.setFont(font);
 81         // 畫邊框。
 82         gd.setColor(Color.BLACK);
 83         gd.drawRect(0, 0, width - 1, height - 1);
 84         // 隨機產生16條干擾線,使圖象中的認證碼不易被其它程序探測到。
 85         gd.setColor(Color.gray);
 86         for (int i = 0; i < interLine; i++) {
 87             int x = random.nextInt(width);
 88             int y = random.nextInt(height);
 89             int xl = random.nextInt(12);
 90             int yl = random.nextInt(12);
 91             gd.drawLine(x, y, x + xl, y + yl);
 92         }
 93         // randomCode用於保存隨機產生的驗證碼,以便用戶登錄后進行驗證。
 94         StringBuffer randomCode = new StringBuffer();
 95         int red = 0, green = 0, blue = 0;
 96         // 隨機產生codeCount數字的驗證碼。
 97         for (int i = 0; i < codeCount; i++) {
 98             // 得到隨機產生的驗證碼數字。
 99             String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
100             // 產生隨機的顏色分量來構造顏色值,這樣輸出的每位數字的顏色值都將不同。
101             red = random.nextInt(255);
102             green = random.nextInt(255);
103             blue = random.nextInt(255);
104             // 用隨機產生的顏色將驗證碼繪制到圖像中。
105             gd.setColor(new Color(red,green,blue));
106             gd.drawString(strRand, (i + 1) * codeX, codeY);
107             // 將產生的四個隨機數組合在一起。
108             randomCode.append(strRand);
109         }
110         result.put(BUFFIMG_KEY, buffImg);
111         result.put(SESSION_KEY, randomCode.toString());
112         return result;
113     }
114     /**
115      * 定時刪除session中存在的驗證碼信息
116      * @param session
117      */
118     public static void removeAttrbute(final HttpSession session) {
119         final Timer timer = new Timer();
120         timer.schedule(new TimerTask() {
121             @Override
122             public void run() {
123                 session.removeAttribute(SESSION_KEY);
124                 timer.cancel();
125             }
126         }, VERIFYCODE_TIMEOUT);
127     }
128 }
View Code

 

4.2.3 自定義用戶信息類CustomUserDetails 集成實體類TUser並實現security提供的UserDetails 接口

UserDetails是真正用於構建SpringSecurity登錄的安全用戶(UserDetails),也就是說,在springsecurity進行用戶認證的過程中,是通過UserDetails的實現類去獲取用戶信息,然后進行授權驗證的。不明白?沒關系,繼續往下看
 1 package com.shf.security.security.config;
 2 
 3 import com.shf.security.user.entity.TUser;
 4 import org.springframework.security.core.GrantedAuthority;
 5 import org.springframework.security.core.userdetails.UserDetails;
 6 
 7 import java.util.Collection;
 8 
 9 /**
10  * 描述:自定義UserDetails,使UserDetails具有TUser的實體結構
11  *
12  * @Author shf
13  * @Date 2019/4/19 10:30
14  * @Version V1.0
15  **/
16 public class CustomUserDetails extends TUser implements UserDetails {
17     public CustomUserDetails(TUser tUser){
18         if(null != tUser){
19             this.setId(tUser.getId());
20             this.setCode(tUser.getCode());
21             this.setCreateTime(tUser.getCreateTime());
22             this.setUpdateTime(tUser.getUpdateTime());
23             this.setUsername(tUser.getUsername());
24             this.setPassword(tUser.getPassword());
25             this.setIsDelete(tUser.getIsDelete());
26             this.setEmail(tUser.getEmail());
27             this.setPhone(tUser.getPhone());
28             this.setRole(tUser.getRole());
29         }
30     }
31     @Override
32     public Collection<? extends GrantedAuthority> getAuthorities() {
33         return null;
34     }
35 
36     @Override
37     public boolean isAccountNonExpired() {
38         return true;
39     }
40 
41     @Override
42     public boolean isAccountNonLocked() {
43         return true;
44     }
45 
46     @Override
47     public boolean isCredentialsNonExpired() {
48         return true;
49     }
50 
51     @Override
52     public boolean isEnabled() {
53         return true;
54     }
55 }

 4.2.4 創建CustomUserDetailsService 類實現UserDetailsService接口

在下文將要提到的CustomAuthenticationProvider 類,也就是security核心的驗證類中,會調用CustomUserDetailsService 中重寫的loadUserByUsername方法

 1 package com.shf.security.security.config;
 2 
 3 import com.shf.security.user.entity.TUser;
 4 import com.shf.security.user.service.TUserService;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.security.core.userdetails.UserDetails;
 7 import org.springframework.security.core.userdetails.UserDetailsService;
 8 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 9 import org.springframework.stereotype.Component;
10 
11 /**
12  * 描述:自定義UserDetailsService,從數據庫讀取用戶信息,實現登錄驗證
13  *
14  * @Author shf
15  * @Date 2019/4/21 17:21
16  * @Version V1.0
17  **/
18 @Component
19 public class CustomUserDetailsService implements UserDetailsService {
20     @Autowired
21     private TUserService userService;
22 
23     /**
24      * 認證過程中 - 根據登錄信息獲取用戶詳細信息
25      *
26      * @param username 登錄用戶輸入的用戶名
27      * @return
28      * @throws UsernameNotFoundException
29      */
30     @Override
31     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
32         //根據用戶輸入的用戶信息,查詢數據庫中已注冊用戶信息
33         TUser user = userService.findByName(username);
34         //如果用戶不存在直接拋出UsernameNotFoundException異常
35         if (user == null) throw new UsernameNotFoundException("用戶名為" + username + "的用戶不存在");
36         return new CustomUserDetails(user);
37     }
38 }

 

4.2.5 新建類CustomWebAuthenticationDetails繼承WebAuthenticationDetails類

類似於UserDetails類給我們提供了用戶詳細信息一樣,WebAuthenticationDetails則為我們提供了登錄請求的用戶的信息(也就是申請登錄的用戶的username和password信息),springsecurity默認只驗證用戶的username和password信息,所以我們如果想實現驗證碼登錄,需要重寫WebAuthenticationDetails類,使其能通過HttpServletRequest獲取到用戶輸入的驗證碼的信息。

 1 package com.shf.security.security.config;
 2 
 3 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 4 
 5 import javax.servlet.http.HttpServletRequest;
 6 
 7 /**
 8  * 描述:自定義WebAuthenticationDetails,將驗證碼和用戶名、密碼一同帶入AuthenticationProvider中
 9  *
10  * @Author shf
11  * @Date 2019/4/21 16:58
12  * @Version V1.0
13  **/
14 public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
15     private static final long serialVersionUID = 6975601077710753878L;
16     private final String verifyCode;
17     public CustomWebAuthenticationDetails(HttpServletRequest request) {
18         super(request);
19         verifyCode = request.getParameter("verifyCode");
20     }
21 
22     public String getVerifyCode() {
23         return verifyCode;
24     }
25 
26     @Override
27     public String toString() {
28         StringBuilder sb = new StringBuilder();
29         sb.append(super.toString()).append("; verifyCode: ").append(this.getVerifyCode());
30         return sb.toString();
31     }
32 }

 

4.2.6 創建CustomAuthenticationDetailsSource類繼承AuthenticationDetailsSource類

上面提到CustomWebAuthenticationDetails 需要通過HttpServletRequest獲取到用戶輸入的驗證碼的信息。AuthenticationDetailsSource類就是初始化CustomWebAuthenticationDetails類的地方,在這里面我們需要將HttpServletRequest傳遞到CustomAuthenticationDetailsSource中。

 1 package com.shf.security.security.config;
 2 
 3 import org.springframework.security.authentication.AuthenticationDetailsSource;
 4 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 5 import org.springframework.stereotype.Component;
 6 
 7 import javax.servlet.http.HttpServletRequest;
 8 
 9 /**
10  * 描述:自定義AuthenticationDetailsSource,將HttpServletRequest注入到CustomWebAuthenticationDetails,使其能獲取到請求中的驗證碼等其他信息
11  *
12  * @Author shf
13  * @Date 2019/4/21 17:03
14  * @Version V1.0
15  **/
16 @Component
17 public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
18     @Override
19     public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
20         return new CustomWebAuthenticationDetails(request);
21     }
22 }

 

4.2.7 實現自定義認證器(重點),創建CustomAuthenticationProvider繼承AbstractUserDetailsAuthenticationProvider類

AbstractUserDetailsAuthenticationProvider類實現的是AuthenticationProvider接口

  1 package com.shf.security.security.config;
  2 
  3 import com.shf.security.utils.VerifyCodeUtil;
  4 import lombok.extern.slf4j.Slf4j;
  5 import org.springframework.beans.factory.annotation.Autowired;
  6 import org.springframework.security.authentication.BadCredentialsException;
  7 import org.springframework.security.authentication.DisabledException;
  8 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  9 import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
 10 import org.springframework.security.core.Authentication;
 11 import org.springframework.security.core.AuthenticationException;
 12 import org.springframework.security.core.userdetails.UserDetails;
 13 import org.springframework.stereotype.Component;
 14 import org.springframework.web.context.request.RequestContextHolder;
 15 import org.springframework.web.context.request.ServletRequestAttributes;
 16 
 17 import javax.servlet.http.HttpServletRequest;
 18 import javax.servlet.http.HttpSession;
 19 
 20 /**
 21  * 描述:自定義SpringSecurity的認證器
 22  *
 23  * @Author shf
 24  * @Date 2019/4/21 17:30
 25  * @Version V1.0
 26  **/
 27 @Component
 28 @Slf4j
 29 public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {//implements AuthenticationProvider {
 30     @Autowired
 31     private CustomUserDetailsService userDetailsService;
 32 
 33     @Override
 34     protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
 35 
 36     }
 37 
 38     @Override
 39     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 40         //用戶輸入的用戶名
 41         String username = authentication.getName();
 42         //用戶輸入的密碼
 43         String password = authentication.getCredentials().toString();
 44         //通過CustomWebAuthenticationDetails獲取用戶輸入的驗證碼信息
 45         CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
 46         String verifyCode = details.getVerifyCode();
 47         if(null == verifyCode || verifyCode.isEmpty()){
 48             log.warn("未輸入驗證碼");
 49             throw new NullPointerException("請輸入驗證碼");
 50         }
 51         //校驗驗證碼
 52         if(!validateVerifyCode(verifyCode)){
 53             log.warn("驗證碼輸入錯誤");
 54             throw new DisabledException("驗證碼輸入錯誤");
 55         }
 56         //通過自定義的CustomUserDetailsService,以用戶輸入的用戶名查詢用戶信息
 57         CustomUserDetails userDetails = (CustomUserDetails) userDetailsService.loadUserByUsername(username);
 58         //校驗用戶密碼
 59         if(!userDetails.getPassword().equals(password)){
 60             log.warn("密碼錯誤");
 61             throw new BadCredentialsException("密碼錯誤");
 62         }
 63         Object principalToReturn = userDetails;
 64         //將用戶信息塞到SecurityContext中,方便獲取當前用戶信息
 65         return this.createSuccessAuthentication(principalToReturn, authentication, userDetails);
 66     }
 67 
 68     @Override
 69     protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
 70         return null;
 71     }
 72 
 73     /**
 74      * 驗證用戶輸入的驗證碼
 75      * @param inputVerifyCode
 76      * @return
 77      */
 78     public boolean validateVerifyCode(String inputVerifyCode){
 79         //獲取當前線程綁定的request對象
 80         HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
 81         // 這個VerifyCodeFactory.SESSION_KEY是在servlet中存入session的名字
 82         HttpSession session = request.getSession();
 83         String verifyCode = (String)session.getAttribute(VerifyCodeUtil.SESSION_KEY);
 84         if(null == verifyCode || verifyCode.isEmpty()){
 85             log.warn("驗證碼過期請重新驗證");
 86             throw new DisabledException("驗證碼過期,請重新驗證");
 87         }
 88         // 不分區大小寫
 89         verifyCode = verifyCode.toLowerCase();
 90         inputVerifyCode = inputVerifyCode.toLowerCase();
 91 
 92         log.info("驗證碼:{}, 用戶輸入:{}", verifyCode, inputVerifyCode);
 93 
 94         return verifyCode.equals(inputVerifyCode);
 95     }
 96 
 97     @Override
 98     public boolean supports(Class<?> authentication) {
 99         return authentication.equals(UsernamePasswordAuthenticationToken.class);
100     }
101 }

 

如上圖所示,AuthenticationProvider接口為我們提供了security核心的認證方法authenticate方法,該方法就是實現用戶認證的方法。我們自定義實現authenticate方法,大致思路如下,通過CustomWebAuthenticationDetails獲取到用戶輸入的username,password,verifyCode信息。通過CustomUserDetails 中獲取用戶信息(數據庫中注冊的用戶的信息),然后對用戶信息進行比對認證。最終實現認證過程。

當然,也可以直接實現AuthenticationProvider 接口,然后實現authenticate方法。這都是可以的但是有現成的AbstractUserDetailsAuthenticationProvider可用,為啥還要再寫一遍呢?尤其是AbstractUserDetailsAuthenticationProvider類提供的createSuccessAuthentication方法,封裝了一個完美的Authentication(后續會繼續提到)。AuthenticationProvider 的supports方法呢是直接決定哪一個AuthenticationProvider 的實現類是我們需要的認證器。

 

4.2.8 創建WebSecurityConfig 繼承WebSecurityConfigurerAdapter配置類。(spring security的配置類)

具體看代碼注釋吧,很詳細的。

值得一提的是第81行的配置,是我們實現ajax登錄的關鍵。

 

  1 package com.shf.security.security.config;
  2 
  3 import lombok.extern.slf4j.Slf4j;
  4 import org.springframework.beans.factory.annotation.Autowired;
  5 import org.springframework.context.annotation.Bean;
  6 import org.springframework.context.annotation.Configuration;
  7 import org.springframework.security.authentication.AuthenticationDetailsSource;
  8 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  9 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 10 import org.springframework.security.config.annotation.web.builders.WebSecurity;
 11 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 13 import org.springframework.security.core.Authentication;
 14 import org.springframework.security.core.AuthenticationException;
 15 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 16 import org.springframework.security.crypto.password.PasswordEncoder;
 17 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 18 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 19 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 20 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
 21 
 22 import javax.servlet.ServletException;
 23 import javax.servlet.http.HttpServletRequest;
 24 import javax.servlet.http.HttpServletResponse;
 25 import java.io.IOException;
 26 import java.io.PrintWriter;
 27 
 28 /**
 29  * 描述:
 30  *
 31  * @Author shf
 32  * @Date 2019/4/19 10:54
 33  * @Version V1.0
 34  **/
 35 @Configuration
 36 @EnableWebSecurity
 37 @Slf4j
 38 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 39     @Autowired
 40     private CustomAuthenticationProvider customAuthenticationProvider;
 41 
 42     @Autowired
 43     private CustomUserDetailsService customUserDetailsService;
 44 
 45     @Autowired
 46     private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
 47 
 48     @Override
 49     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 50         //將自定義的CustomAuthenticationProvider裝配到AuthenticationManagerBuilder
 51         auth.authenticationProvider(customAuthenticationProvider);
 52         //將自定的CustomUserDetailsService裝配到AuthenticationManagerBuilder
 53         auth.userDetailsService(customUserDetailsService).passwordEncoder(new PasswordEncoder() {
 54             @Override
 55             public String encode(CharSequence charSequence) {
 56                 return charSequence.toString();
 57             }
 58 
 59             @Override
 60             public boolean matches(CharSequence charSequence, String s) {
 61                 return s.equals(charSequence.toString());
 62             }
 63         });
 64     }
 65     @Override
 66     public void configure(HttpSecurity http) throws Exception {
 67         http
 68                 .cors()
 69                 .and().csrf().disable();//開啟跨域
 70         http    /*匿名請求:不需要進行登錄攔截的url*/
 71                 .authorizeRequests()
 72                     .antMatchers("/getVerifyCode").permitAll()
 73                     .anyRequest().authenticated()//其他的路徑都是登錄后才可訪問
 74                     .and()
 75                 /*登錄配置*/
 76                 .formLogin()
 77                     .loginPage("/login_page")//登錄頁,當未登錄時會重定向到該頁面
 78                     .successHandler(authenticationSuccessHandler())//登錄成功處理
 79                     .failureHandler(authenticationFailureHandler())//登錄失敗處理
 80                     .authenticationDetailsSource(authenticationDetailsSource)//自定義驗證邏輯,增加驗證碼信息
 81                     .loginProcessingUrl("/login")//restful登錄請求地址
 82                     .usernameParameter("username")//默認的用戶名參數
 83                     .passwordParameter("password")//默認的密碼參數
 84                     .permitAll()
 85                     .and()
 86                 /*登出配置*/
 87                 .logout()
 88                     .permitAll()
 89                     .logoutSuccessHandler(logoutSuccessHandler());
 90     }
 91 
 92     /**
 93      * security檢驗忽略的請求,比如靜態資源不需要登錄的可在本處配置
 94      * @param web
 95      */
 96     @Override
 97     public void configure(WebSecurity web){
 98 //        platform.ignoring().antMatchers("/");
 99     }
100 
101     @Autowired
102     public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
103         auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
104         auth.eraseCredentials(false);
105     }
106     //密碼加密配置
107     @Bean
108     public BCryptPasswordEncoder passwordEncoder() {
109         return new BCryptPasswordEncoder(4);
110     }
111     //登入成功
112     @Bean
113     public AuthenticationSuccessHandler authenticationSuccessHandler() {
114         return new AuthenticationSuccessHandler() {
115             /**
116              * 處理登入成功的請求
117              *
118              * @param httpServletRequest
119              * @param httpServletResponse
120              * @param authentication
121              * @throws IOException
122              * @throws ServletException
123              */
124             @Override
125             public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
126                 httpServletResponse.setContentType("application/json;charset=utf-8");
127                 PrintWriter out = httpServletResponse.getWriter();
128                 out.write("{\"status\":\"success\",\"msg\":\"登錄成功\"}");
129                 out.flush();
130                 out.close();
131             }
132         };
133     }
134     //登錄失敗
135     @Bean
136     public AuthenticationFailureHandler authenticationFailureHandler(){
137         return new AuthenticationFailureHandler() {
138             /**
139              * 處理登錄失敗的請求
140              * @param httpServletRequest
141              * @param httpServletResponse
142              * @param e
143              * @throws IOException
144              * @throws ServletException
145              */
146             @Override
147             public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
148                 httpServletResponse.setContentType("application/json;charset=utf-8");
149                 PrintWriter out = httpServletResponse.getWriter();
150                 out.write("{\"status\":\"error\",\"msg\":\"登錄失敗\"}");
151                 out.flush();
152                 out.close();
153             }
154         };
155     }
156     //登出處理
157     @Bean
158     public LogoutSuccessHandler logoutSuccessHandler() {
159         return new LogoutSuccessHandler() {
160             /**
161              * 處理登出成功的請求
162              *
163              * @param httpServletRequest
164              * @param httpServletResponse
165              * @param authentication
166              * @throws IOException
167              * @throws ServletException
168              */
169             @Override
170             public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
171                 httpServletResponse.setContentType("application/json;charset=utf-8");
172                 PrintWriter out = httpServletResponse.getWriter();
173                 out.write("{\"status\":\"success\",\"msg\":\"登出成功\"}");
174                 out.flush();
175                 out.close();
176             }
177         };
178     }
179 }

 

 

4.2.9 LoginController

 1 package com.shf.security.login;
 2 
 3 import com.shf.security.utils.Response;
 4 import com.shf.security.utils.VerifyCodeUtil;
 5 import org.springframework.web.bind.annotation.RequestMapping;
 6 import org.springframework.web.bind.annotation.RestController;
 7 
 8 import javax.imageio.ImageIO;
 9 import javax.servlet.ServletOutputStream;
10 import javax.servlet.http.HttpServletRequest;
11 import javax.servlet.http.HttpServletResponse;
12 import javax.servlet.http.HttpSession;
13 import java.awt.image.RenderedImage;
14 import java.io.IOException;
15 import java.util.HashMap;
16 import java.util.Map;
17 
18 /**
19  * 描述:
20  *
21  * @Author shf
22  * @Date 2019/4/19 14:58
23  * @Version V1.0
24  **/
25 @RestController
26 public class LoginController {
27     @RequestMapping("/login_error")
28     public Response loginError(){
29         Response response = new Response();
30         response.buildSuccessResponse("登錄失敗");
31         return response;
32     }
33     @RequestMapping("/login_success")
34     public Response loginSuccess(){
35         Response response = new Response();
36         response.buildSuccessResponse("登錄成功");
37         return response;
38     }
39 
40     @RequestMapping("/login_page")
41     public Response root(){
42         Response response = new Response();
43         response.buildSuccessResponse("尚未登錄,請登錄");
44         return response;
45     }
46 
47     @RequestMapping("/getVerifyCode")
48     public void getVerifyCode(HttpServletRequest request, HttpServletResponse response){
49         Map<String, Object> map = VerifyCodeUtil.getVerifyCode();
50         HttpSession session = request.getSession();
51         session.setAttribute(VerifyCodeUtil.SESSION_KEY, map.get(VerifyCodeUtil.SESSION_KEY));
52         // 禁止圖像緩存。
53         response.setHeader("Pragma", "no-cache");
54         response.setHeader("Cache-Control", "no-cache");
55         response.setDateHeader("Expires", 0);
56         response.setContentType("image/jpeg");
57         // 將圖像輸出到Servlet輸出流中。
58         try {
59             ServletOutputStream sos = response.getOutputStream();
60             ImageIO.write((RenderedImage) map.get(VerifyCodeUtil.BUFFIMG_KEY), "jpeg", sos);
61             sos.close();
62             //設置驗證碼過期時間
63             VerifyCodeUtil.removeAttrbute(session);
64         } catch (IOException e) {
65             e.printStackTrace();
66         }
67     }
68 }
View Code

 

 4.2.10 UserHolder 工具類

在日常的業務中,在很多業務代碼中,我們都需要獲取當前用戶的信息。這個類就是一個靜態工具類。

 1 package com.shf.security.utils;
 2 
 3 import com.shf.security.user.entity.TUser;
 4 import org.springframework.security.core.Authentication;
 5 import org.springframework.security.core.context.SecurityContext;
 6 import org.springframework.security.core.context.SecurityContextHolder;
 7 
 8 /**
 9  * 描述:
10  *
11  * @Author shf
12  * @Description TODO
13  * @Date 2019/4/21 15:24
14  * @Version V1.0
15  **/
16 public class UserHolder {
17     public static TUser getUserDetail(){
18         SecurityContext ctx = SecurityContextHolder.getContext();
19         Authentication auth = ctx.getAuthentication();
20         TUser user = (TUser) auth.getPrincipal();
21         return user;
22     }
23     public static String getUserCode(){
24         SecurityContext ctx = SecurityContextHolder.getContext();
25         Authentication auth = ctx.getAuthentication();
26         TUser user = (TUser) auth.getPrincipal();
27         return user.getCode();
28     }
29     public static int getUserId(){
30         SecurityContext ctx = SecurityContextHolder.getContext();
31         Authentication auth = ctx.getAuthentication();
32         TUser user = (TUser) auth.getPrincipal();
33         return user.getId();
34     }
35 }

 4.2.10 其他工具類Response.java

 1 package com.shf.security.utils;
 2 
 3 import lombok.Data;
 4 
 5 /**
 6  * 描述:
 7  *
 8  * @Author shf
 9  * @Description TODO
10  * @Date 2019/4/16 15:03
11  * @Version V1.0
12  **/
13 @Data
14 public class Response {
15     private String code;
16     private String msg;
17     private Object data;
18     public Response() {
19         this.code = "-200";
20         this.msg = "SUCCESS";
21     }
22     public Response(String code, String msg){
23         this.code = code;
24         this.msg = msg;
25     }
26     public Response buildSuccessResponse(){
27         this.code = "-200";
28         this.msg = "SUCCESS";
29         return this;
30     }
31     public Response buildFailedResponse(){
32         this.code = "-400";
33         this.msg = "FAILED";
34         return this;
35     }
36     public Response buildSuccessResponse(String msg){
37         this.code = "-200";
38         this.msg = msg;
39         return this;
40     }
41     public Response buildFailedResponse(String msg){
42         this.code = "-400";
43         this.msg = msg;
44         return this;
45     }
46     public Response buildFailedResponse(String code, String msg){
47         this.code = code;
48         this.msg = msg;
49         return this;
50     }
51     public Response buildSuccessResponse(String code, String msg){
52         this.code = code;
53         this.msg =  msg;
54         return this;
55     }
56 }
View Code

 

五、問題總結

5.1 驗證碼問題

其實呢通過第二部分對security原理的分析,我們不難看出,spring security就是建立在一連串的過濾器filter上的,spring security通過這些過濾器逐層對請求進行過濾,然后進行各種登錄認證和授權過程。說道這里估計大家也就能想到另外的實現驗證碼驗證登錄的方式。也就是在認證用戶輸入的用戶名和密碼之前驗證驗證碼信息。UsernamePasswordAuthenticationFilter過濾器顧名思義就是用戶名和密碼的過濾器。所以我們只需要在4.2.8 章節中的WebSecurityConfig中addFilterBefore()配置在UsernamePasswordAuthenticationFilter過濾器之前執行VerifyCodeFilter過濾器。然后在VerifyCodeFilter過濾器中執行驗證碼的驗證邏輯即可。

1 .and()
2 .addFilterBefore(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class)

 

但是這種方式呢有一種天然的缺點,也就是沒法辦將除username和password的信息帶到認證器中進行統一認證。而且如果我們除了驗證碼意外還需要驗證更多的信息的話。豈不是要寫n多個filter。

5.2  貌似忘了進行測試登錄

瀏覽器請求:http://localhost:8080/user/test

結果:

正是我們想要的結果。

登錄驗證還是使用postman吧,因為spring security默認只處理post方式的登錄請求。瀏覽器提交restful請求默認是get的。所以。。。

postman請求驗證碼

 postman登錄

 

看到這里如果還有問題,請移步https://github.com/hello-shf/spring-security.git開箱即用。

如有問題或者錯誤的地方,還請留言指出。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM