一、文章簡介
本文簡要介紹了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');
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>
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 }
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 }
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 }
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 }
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 }
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 }
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 }
五、問題總結
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開箱即用。
如有問題或者錯誤的地方,還請留言指出。