Spring Security自定義登錄認證校驗用戶名、密碼,自定義密碼加密方式,以及在前后端分離的情況下認證失敗或成功處理返回json格式數據等
Spring Security 自定義登錄認證處理
基本環境
- spring-boot 2.4.1 (最新版本,如有問題,后期會調整)
- mybatis-plus 2.2.0 (沒有采用最新版本,后期會弄個簡單的代碼生成器)
- mysql
- maven項目
數據庫用戶信息表t_sys_user 本人采用的是5.6還是5.7版本來的,不是8.0
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_sys_user -- ---------------------------- DROP TABLE IF EXISTS `t_sys_user`; CREATE TABLE `t_sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID', `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '賬號', `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '登錄密碼', `nick_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵稱', `sex` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性別 0:男 1:女', `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手機號碼', `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '郵箱', `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '頭像', `flag` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '狀態', `salt` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '鹽值', `token` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'token', `qq_oppen_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'QQ 第三方登錄Oppen_ID唯一標識', `pwd` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '明文密碼', `gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '創建時間', `gmt_modified` datetime(0) NULL DEFAULT NULL COMMENT '更新時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系統管理-用戶基礎信息表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of t_sys_user -- ---------------------------- INSERT INTO `t_sys_user` VALUES (1, 'admin', '97ba1ef7f148b2aec1c61303a7d88d0967825495', '鄭某某', '0', '15183303003', '10086@qq.com', 'http://qzapp.qlogo.cn/qzapp/101536330/86F96F92387D69BD7659C4EC3CD6BD69/100', '1', 'zhengqing', '20820cef877355ad636b9e938c533e5cc1152e4f', '', '123456', '2019-05-05 16:09:06', '2019-10-23 17:26:30'); INSERT INTO `t_sys_user` VALUES (2, 'test', '97ba1ef7f148b2aec1c61303a7d88d0967825495', '測試號', '0', '10000', '10000@qq.com', 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', '1', 'zhengqing', '2425fb04b4bcb140e05d22d46baa9c257ceed879', NULL, '123456', '2019-05-05 16:15:06', '2019-10-23 16:56:38'); SET FOREIGN_KEY_CHECKS = 1;
到此步需要使用的pom.xml文件
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.4.1</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Security依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- mybatis-plus begin =================================== --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>2.2.0</version> </dependency> <!-- ========================= 數據庫相關 ========================== --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 阿里數據庫連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.18</version> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> <exclusions> <exclusion> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> </exclusion> </exclusions> </dependency> <!-- swagger-ui --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-bean-validators</artifactId> <version>2.6.1</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 阿里FastJson轉換工具依賴 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.13</version> </dependency> <!-- AOP依賴 【注:系統日記需要此依賴】 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- Hibernate Validator提供的注解進行參數校驗 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> <optional>true</optional> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.18.Final</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>
yml的配置
server: port: 8083 spring: application: name: security # 配置數據源 datasource: # MySQL在高版本需要指明是否進行SSL連接 解決則加上 &useSSL=false url: jdbc:mysql://127.0.0.1:3306/white_jotter?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=UTC name: white_jotter username: root password: 123456 # mysql5連接驅動 driverClassName: com.mysql.cj.jdbc.Driver maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20 security: user: name: admin # 用戶名 password: 123456 # 密碼 # 關閉安全驗證 management: security: enabled: false # mybatis-plus相關配置 mybatis-plus: # xml掃描,多個目錄用逗號或者分號分隔(告訴 Mapper 所對應的 XML 文件位置) mapper-locations: classpath:cn/com/sercurity/cyy/**/mapper/xml/*Mapper.xml # 以下配置均有默認值,可以不設置 global-config: #主鍵類型 0:"數據庫ID自增", 1:"用戶輸入ID",2:"全局唯一ID (數字類型唯一ID)", 3:"全局唯一ID UUID"; id-type: 0 #字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷" field-strategy: 2 #駝峰下划線轉換 db-column-underline: true #刷新mapper 調試神器 refresh-mapper: false #數據庫大寫下划線轉換 #capital-mode: true #序列接口實現類配置 #key-generator: com.baomidou.springboot.xxx #邏輯刪除配置 #logic-delete-value: 0 # 邏輯已刪除值(默認為 1) #logic-not-delete-value: 1 # 邏輯未刪除值(默認為 0) #自定義填充策略接口實現 # meta-object-handler: xxxxxx #自定義SQL注入器 #sql-injector: com.baomidou.springboot.xxx configuration: # 是否開啟自動駝峰命名規則映射:從數據庫列名到Java屬性駝峰命名的類似映射 map-underscore-to-camel-case: true cache-enabled: false # 如果查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段 # call-setters-on-nulls: true # 這個配置會將執行的sql打印出來,在開發或測試的時候可以用 # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 解決oracle更新數據為null時無法轉換報錯,mysql不會出現此情況 jdbc-type-for-null: 'null' cyy: swagger: title: 測試項目demo接口文檔 description: 測試項目demo接口文檔 version: 1.0.0 termsOfServiceUrl: contactName: contactUrl: contactEmail: license: licenseUrl: #安全認證 auth: # token過期時間(分鍾) tokenExpireTime: 60 # 用戶選擇保存登錄狀態對應token過期時間(天) saveLoginTime: 7 # 限制用戶登陸錯誤次數(次) loginTimeLimit: 10 # 錯誤超過次數后多少分鍾后才能繼續登錄(分鍾) loginAfterTime: 10 ignoreUrls: - /login - /api/system/user/getCurrentUserInfo - /index - /logout - /swagger-ui.html # - /swagger-resources/** - /swagger-resources/configuration/ui - /swagger-resources - /v2/api-docs - /swagger-resources/configuration/security # - /swagger/** # - /**/v2/api-docs - /**/*.js - /**/*.css - /**/*.png - /**/*.ico
接下來的代碼可能會有些亂,在這里貼一下項目目錄吧,方便大家查看
t_sys_user表的實體類
import cn.com.sercurity.cyy.common.entity.BaseEntity; import cn.com.sercurity.cyy.common.validator.Create; import cn.com.sercurity.cyy.common.validator.FieldRepeatValidator; import cn.com.sercurity.cyy.common.validator.Update; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.io.Serializable; /** * <p> * user實體類 * </p> * * @author cyy * @since 2020-12-30 13:34 */ /** * <p> 系統管理-用戶基礎信息表 </p> * * @author : cyy * @date : 2020/12/30 15:40 */ @Data @ApiModel(description = "系統管理-用戶基礎信息表") @TableName("t_sys_user") /** * 對注解分組的排序,可以通脫他判斷先后順序 * @GroupSequence({FieldRepeatValidator.class,NotNull.class, Default.class}) */ @FieldRepeatValidator(field = "username", message = "賬號重復,請重新輸入賬號!") public class User extends BaseEntity<User> { private static final long serialVersionUID = 1L; /** * 主鍵ID groups:標識在更新的時候才能驗證非空 */ @ApiModelProperty(value = "主鍵ID") @TableId(value="id", type= IdType.AUTO) @NotNull(message = "用戶id不能為空", groups={Update.class}) private Integer id; /** * 賬號 */ @ApiModelProperty(value = "賬號") @TableField("username") @NotBlank(message = "賬號不能為空", groups = {Create.class, Update.class}) @Length(max = 100, message = "賬號不能超過100個字符") @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "賬號限制:最多100字符,包含文字、字母和數字") private String username; /** * 登錄密碼 */ @ApiModelProperty(value = "登錄密碼") @TableField("password") private String password; /** * 明文密碼 - QQ第三方授權登錄時用 */ @ApiModelProperty(value = "明文密碼") @TableField("pwd") @NotBlank(message = "密碼不能為空") // @FieldRepeatValidator(className = "com.zhengqing.modules.system.entity.User", field = "pwd", message = "密碼重復!") // @FieldRepeatValidator(className = "com.zhengqing.modules.system.entity.User", field = "pwd", message = "密碼重復!",groups={FieldRepeatValidator.class}) private String pwd; /** * 昵稱 */ @ApiModelProperty(value = "昵稱") @TableField("nick_name") @NotBlank(message = "昵稱不能為空") private String nickName; /** * 性別 0:男 1:女 */ @ApiModelProperty(value = "性別 0:男 1:女") @TableField("sex") private String sex; /** * 手機號碼 */ @ApiModelProperty(value = "手機號碼") @TableField("phone") @NotBlank(message = "手機號不能為空") @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手機號格式有誤") private String phone; /** * 郵箱 */ @ApiModelProperty(value = "郵箱") @TableField("email") @NotBlank(message = "聯系郵箱不能為空") @Email(message = "郵箱格式不對") private String email; /** * 頭像 */ @ApiModelProperty(value = "頭像") @TableField("avatar") private String avatar; /** * 狀態 */ @ApiModelProperty(value = "狀態") @TableField("flag") private String flag; /** * 鹽值 */ @ApiModelProperty(value = "鹽值") @TableField("salt") private String salt; /** * token */ @ApiModelProperty(value = "token") @TableField("token") private String token; @ApiModelProperty(value = "QQ 第三方登錄Oppen_ID唯一標識") @TableField("qq_oppen_id") private String qqOppenId; @Override protected Serializable pkVal() { return this.id; } }
import com.baomidou.mybatisplus.activerecord.Model; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.enums.FieldFill; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; import lombok.Setter; import java.util.Date; @Getter @Setter public abstract class BaseEntity<T extends Model> extends BaseAddEntity<T> { /** * 修改時間 - 過去分詞表示被動更新 */ @ApiModelProperty(value = "修改時間") @TableField(value = "gmt_modified", fill = FieldFill.INSERT_UPDATE) private Date gmtModified; }
import com.baomidou.mybatisplus.activerecord.Model; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.enums.FieldFill; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.Past; import java.util.Date; @Getter @Setter public abstract class BaseAddEntity<T extends Model> extends Model<T>{ /** * 創建日期 - 現在時表示主動創建 */ @ApiModelProperty(value = "創建日期") @TableField(value = "gmt_create", fill = FieldFill.INSERT) @Past(message = "創建時間必須是過去時間") private Date gmtCreate; }
import javax.validation.groups.Default; /** * <p> 使用groups的校驗 </p> * * @description : 同一個對象要復用,比如UserDTO在更新時候要校驗userId,在保存的時候不需要校驗userId,在兩種情況下都要校驗username,那就用上groups了 * 在需要校驗的地方@Validated聲明校驗組 ` update(@RequestBody @Validated(Update.class) UserDTO userDTO) ` * 在DTO中的字段上定義好groups = {}的分組類型 ` @NotNull(message = "用戶id不能為空", groups = Update.class) 或 groups = {Create.class, Update.class} * private Long userId; ` * 【注】注意:在聲明分組的時候盡量加上 extend javax.validation.groups.Default 否則,在你聲明@Validated(Update.class)的時候,就會出現你在默認沒添加groups = {}的時候的校驗組@Email(message = "郵箱格式不對"),會不去校驗,因為默認的校驗組是groups = {Default.class}.*/ public interface Create extends Default { }
import javax.validation.groups.Default; /** * <p> 使用groups的校驗 </p> * * @description : 同一個對象要復用,比如UserDTO在更新時候要校驗userId,在保存的時候不需要校驗userId,在兩種情況下都要校驗username,那就用上groups了 * 在需要校驗的地方@Validated聲明校驗組 ` update(@RequestBody @Validated(Update.class) UserDTO userDTO) ` * 在DTO中的字段上定義好groups = {}的分組類型 ` @NotNull(message = "用戶id不能為空", groups = Update.class) 或 groups = {Create.class, Update.class} * private Long userId; ` * 【注】注意:在聲明分組的時候盡量加上 extend javax.validation.groups.Default 否則,在你聲明@Validated(Update.class)的時候,就會出現你在默認沒添加groups = {}的時候的校驗組@Email(message = "郵箱格式不對"),會不去校驗,因為默認的校驗組是groups = {Default.class}*/ public interface Update extends Default{ }
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; /** * <p> 自定義字段對應數據庫內容重復校驗 注解 </p> * */ // 元注解: 給其他普通的標簽進行解釋說明 【@Retention、@Documented、@Target、@Inherited、@Repeatable】 @Documented /** * 指明生命周期: * RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。 * RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。 * RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。 */ @Retention(RetentionPolicy.RUNTIME) /** * 指定注解運用的地方: * ElementType.ANNOTATION_TYPE 可以給一個注解進行注解 * ElementType.CONSTRUCTOR 可以給構造方法進行注解 * ElementType.FIELD 可以給屬性進行注解 * ElementType.LOCAL_VARIABLE 可以給局部變量進行注解 * ElementType.METHOD 可以給方法進行注解 * ElementType.PACKAGE 可以給一個包進行注解 * ElementType.PARAMETER 可以給一個方法內的參數進行注解 * ElementType.TYPE 可以給一個類型進行注解,比如類、接口、枚舉 * @Repeatable(LinkVals.class)(可重復注解同一字段,或者類,java1.8后支持) * @author : cyy */ @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE}) @Constraint(validatedBy = FieldRepeatValidatorClass.class) public @interface FieldRepeatValidator { /** * 實體類id字段 - 默認為id (該值可無) * @return * @author : cyy */ String id() default "id";; /** * 注解屬性 - 對應校驗字段 * @return * @author : cyy */ String field(); /** * 默認錯誤提示信息 * @return * @author : cyy */ String message() default "字段內容重復!"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
關於實體類所涉及到的代碼如上,如果有缺少部分,請留言,會補充的
1、Security 核心配置類 配置用戶密碼校驗過濾器
import cn.com.sercurity.cyy.config.security.filter.AdminAuthenticationProcessingFilter; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 用戶密碼校驗過濾器 */ private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter; public SecurityConfig(AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter) { this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter; } /** * 權限配置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests(); // 禁用CSRF 開啟跨域 http.csrf().disable().cors(); // 登錄處理 - 前后端一體的情況下 // registry.and().formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll() // // 自定義登陸用戶名和密碼屬性名,默認為 username和password // .usernameParameter("username").passwordParameter("password") // // 異常處理 // .failureUrl("/login/error").permitAll() // // 退出登錄 // .and().logout().permitAll(); // 標識只能在 服務器本地ip[127.0.0.1或localhost] 訪問`/home`接口,其他ip地址無法訪問 registry.antMatchers("/home").hasIpAddress("127.0.0.1"); // 允許匿名的url - 可理解為放行接口 - 多個接口使用,分割 registry.antMatchers("/login", "/index").permitAll(); // OPTIONS(選項):查找適用於一個特定網址資源的通訊選擇。 在不需執行具體的涉及數據傳輸的動作情況下, 允許客戶端來確定與資源相關的選項以及 / 或者要求, 或是一個服務器的性能 registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll(); // 自動登錄 - cookie儲存方式 registry.and().rememberMe(); // 其余所有請求都需要認證 registry.anyRequest().authenticated(); // 防止iframe 造成跨域 registry.and().headers().frameOptions().disable(); // 自定義過濾器認證用戶名密碼 http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class); } }
2、自定義用戶密碼校驗過濾器
import cn.com.sercurity.cyy.common.util.Constants; import cn.com.sercurity.cyy.common.util.MultiReadHttpServletRequest; import cn.com.sercurity.cyy.config.security.login.AdminAuthenticationFailureHandler; import cn.com.sercurity.cyy.config.security.login.AdminAuthenticationSuccessHandler; import cn.com.sercurity.cyy.config.security.login.CusAuthenticationManager; import cn.com.sercurity.cyy.user.entity.User; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j @Component public class AdminAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { /** * @param authenticationManager: 認證管理器 * @param adminAuthenticationSuccessHandler: 認證成功處理 * @param adminAuthenticationFailureHandler: 認證失敗處理 */ public AdminAuthenticationProcessingFilter(CusAuthenticationManager authenticationManager, AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler, AdminAuthenticationFailureHandler adminAuthenticationFailureHandler) { super(new AntPathRequestMatcher("/login", "POST")); this.setAuthenticationManager(authenticationManager); this.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler); this.setAuthenticationFailureHandler(adminAuthenticationFailureHandler); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (request.getContentType() == null || !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE)) { throw new AuthenticationServiceException("請求頭類型不支持: " + request.getContentType()); } UsernamePasswordAuthenticationToken authRequest; try { MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request); // 將前端傳遞的數據轉換成jsonBean數據格式 User user = JSONObject.parseObject(wrappedRequest.getBodyJsonStrByJson(wrappedRequest), User.class); authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), null); authRequest.setDetails(authenticationDetailsSource.buildDetails(wrappedRequest)); } catch (Exception e) { throw new AuthenticationServiceException(e.getMessage()); } return this.getAuthenticationManager().authenticate(authRequest); } }
3、自定義認證管理器
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderNotFoundException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import java.util.Objects; @Component public class CusAuthenticationManager implements AuthenticationManager { private final AdminAuthenticationProvider adminAuthenticationProvider; public CusAuthenticationManager(AdminAuthenticationProvider adminAuthenticationProvider) { this.adminAuthenticationProvider = adminAuthenticationProvider; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Authentication result = adminAuthenticationProvider.authenticate(authentication); if (Objects.nonNull(result)) { return result; } throw new ProviderNotFoundException("Authentication failed!"); } }
認證成功處理
import cn.com.sercurity.cyy.common.dto.ApiResult; import cn.com.sercurity.cyy.common.util.ResponseUtils; import cn.com.sercurity.cyy.config.security.dto.SecurityUser; import cn.com.sercurity.cyy.user.entity.User; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication auth) throws IOException, ServletException { User user = new User(); SecurityUser securityUser = ((SecurityUser) auth.getPrincipal()); user.setToken(securityUser.getCurrentUserInfo().getToken()); ResponseUtils.out(response, ApiResult.ok("登錄成功!", user)); } }
認證失敗處理 - 前后端分離情況下返回json數據格式
import cn.com.sercurity.cyy.common.dto.ApiResult; import cn.com.sercurity.cyy.common.util.ResponseUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ApiResult result; if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) { result = ApiResult.fail(e.getMessage()); } else if (e instanceof LockedException) { result = ApiResult.fail("賬戶被鎖定,請聯系管理員!"); } else if (e instanceof CredentialsExpiredException) { result = ApiResult.fail("證書過期,請聯系管理員!"); } else if (e instanceof AccountExpiredException) { result = ApiResult.fail("賬戶過期,請聯系管理員!"); } else if (e instanceof DisabledException) { result = ApiResult.fail("賬戶被禁用,請聯系管理員!"); } else { log.error("登錄失敗:", e); result = ApiResult.fail("登錄失敗!"); } ResponseUtils.out(response, result); } }
全局常用變量
import java.util.HashMap; import java.util.Map; public class Constants { /** * 接口url */ public static Map<String,String> URL_MAPPING_MAP = new HashMap<>(); /** * 獲取項目根目錄 */ public static String PROJECT_ROOT_DIRECTORY = System.getProperty("user.dir"); /** * 密碼加密相關 */ public static String SALT = "cyy"; public static final int HASH_ITERATIONS = 1; /** * 請求頭 - token */ public static final String REQUEST_HEADER = "X-Token"; /** * 請求頭類型: * application/x-www-form-urlencoded : form表單格式 * application/json : json格式 */ public static final String REQUEST_HEADERS_CONTENT_TYPE = "application/json"; /** * 登錄者角色 */ public static final String ROLE_LOGIN = "role_login"; }
多次讀寫BODY用HTTP REQUEST - 解決流只能讀一次問題
import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @Slf4j public class MultiReadHttpServletRequest extends HttpServletRequestWrapper { private final byte[] body; public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException { super(request); body = getBodyString(request).getBytes(Charset.forName("UTF-8")); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return bais.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } /** * 獲取請求Body * * @param request * @return */ private String getBodyString(ServletRequest request) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null; BufferedReader reader = null; try { inputStream = request.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return sb.toString(); } /** * 將前端請求的表單數據轉換成json字符串 - 前后端一體的情況下使用 * @param request: * @return: java.lang.String */ public String getBodyJsonStrByForm(ServletRequest request){ Map<String, Object> bodyMap = new HashMap<>(16); try { // 參數定義 String paraName = null; // 獲取請求參數並轉換 Enumeration<String> e = request.getParameterNames(); while (e.hasMoreElements()) { paraName = e.nextElement(); bodyMap.put(paraName, request.getParameter(paraName)); } } catch(Exception e) { log.error("請求參數轉換錯誤!",e); } return JSONObject.toJSONString(bodyMap); } /** * 將前端傳遞的json數據轉換成json字符串 - 前后端分離的情況下使用 * @param request: * @return: java.lang.String */ public String getBodyJsonStrByJson(ServletRequest request){ StringBuffer json = new StringBuffer(); String line = null; try { BufferedReader reader = request.getReader(); while((line = reader.readLine()) != null) { json.append(line); } } catch(Exception e) { log.error("請求參數轉換錯誤!",e); } return json.toString(); } }
API返回參數
import cn.com.sercurity.cyy.common.enumeration.ResultCode; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @ApiModel(value = "API返回參數") public class ApiResult { /** * 消息內容 */ @ApiModelProperty(value = "響應消息", required = false) private String message; /** * 響應碼:參考`ResultCode` */ @ApiModelProperty(value = "響應碼", required = true) private Integer code; /** * 響應中的數據 */ @ApiModelProperty(value = "響應數據", required = false) private Object data; /*** * 過期 * * @param message: */ public static ApiResult expired(String message) { return new ApiResult(ResultCode.UN_LOGIN.getCode(), message, null); } public static ApiResult fail(String message) { return new ApiResult(ResultCode.FAILURE.getCode(), message, null); } /*** * 自定義錯誤返回碼 * * @param code * @param message: */ public static ApiResult fail(Integer code, String message) { return new ApiResult(code, message, null); } public static ApiResult ok(String message) { return new ApiResult(ResultCode.SUCCESS.getCode(), message, null); } public static ApiResult ok() { return new ApiResult(ResultCode.SUCCESS.getCode(), "OK", null); } public static ApiResult build(Integer code, String msg, Object data) { return new ApiResult(ResultCode.SUCCESS.getCode(), msg, data); } public static ApiResult ok(String message, Object data) { return new ApiResult(ResultCode.SUCCESS.getCode(), message, data); } /** * 自定義返回碼 */ public static ApiResult ok(Integer code, String message) { return new ApiResult(code, message); } /** * 自定義 * * @param code:驗證碼 * @param message:返回消息內容 * @param data:返回數據 */ public static ApiResult ok(Integer code, String message, Object data) { return new ApiResult(code, message, data); } public ApiResult() { } public static ApiResult build(Integer code, String msg) { return new ApiResult(code, msg, null); } public ApiResult(Integer code, String msg, Object data) { this.code = code; this.message = msg; this.data = data; } public ApiResult(Object data) { this.code = ResultCode.SUCCESS.getCode(); this.message = "OK"; this.data = data; } public ApiResult(String message) { this(ResultCode.SUCCESS.getCode(), message, null); } public ApiResult(String message, Integer code) { this.message = message; this.code = code; } public ApiResult(Integer code, String message) { this.code = code; this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
響應碼枚舉
public enum ResultCode { //成功 SUCCESS( 200, "SUCCESS" ), //失敗 FAILURE( 400, "FAILURE" ), // 未登錄 UN_LOGIN( 401, "未登錄" ), //未認證(簽名錯誤、token錯誤) UNAUTHORIZED( 403, "未認證或Token失效" ), //未通過認證 USER_UNAUTHORIZED( 402, "用戶名或密碼不正確" ), //接口不存在 NOT_FOUND( 404, "接口不存在" ), //服務器內部錯誤 INTERNAL_SERVER_ERROR( 500, "服務器內部錯誤" ); private int code; private String desc; ResultCode(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } }
使用response輸出JSON
import cn.com.sercurity.cyy.common.dto.ApiResult; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Slf4j public class ResponseUtils { /** * 使用response輸出JSON * * @param response * @param result */ public static void out(ServletResponse response, ApiResult result) { PrintWriter out = null; try { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); out = response.getWriter(); out.println(JSON.toJSONString(result)); } catch (Exception e) { log.error(e + "輸出JSON出錯"); } finally { if (out != null) { out.flush(); out.close(); } } } /** * 響應內容 * @param httpServletResponse * @param msg * @param status */ public static void getResponse(HttpServletResponse httpServletResponse, String msg, Integer status){ PrintWriter writer = null; httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); try { writer = httpServletResponse.getWriter(); writer.print(JSONObject.toJSONString(new ApiResult(status,msg,null))); } catch (IOException e) { log.error("響應報錯", e.getMessage()); } finally { if (writer != null){ writer.close(); } } } }
4、自定義認證處理
import cn.com.sercurity.cyy.common.util.PasswordUtils; import cn.com.sercurity.cyy.config.security.dto.SecurityUser; import cn.com.sercurity.cyy.config.security.service.impl.UserDetailsServiceImpl; import cn.com.sercurity.cyy.user.entity.User; import cn.com.sercurity.cyy.user.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; @Component public class AdminAuthenticationProvider implements AuthenticationProvider { @Autowired UserDetailsServiceImpl userDetailsService; @Autowired private UserMapper userMapper; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 獲取前端表單中輸入后返回的用戶名、密碼 String userName = (String) authentication.getPrincipal(); String password = (String) authentication.getCredentials(); SecurityUser userInfo = (SecurityUser) userDetailsService.loadUserByUsername(userName); boolean isValid = PasswordUtils.isValidPassword(password, userInfo.getPassword(), userInfo.getCurrentUserInfo().getSalt()); // 驗證密碼 if (!isValid) { throw new BadCredentialsException("密碼錯誤!"); } // 前后端分離情況下 處理邏輯... // 更新登錄令牌 - 之后訪問系統其它接口直接通過token認證用戶權限... String token = PasswordUtils.encodePassword(System.currentTimeMillis() + userInfo.getCurrentUserInfo().getSalt(), userInfo.getCurrentUserInfo().getSalt()); User user = userMapper.selectById(userInfo.getCurrentUserInfo().getId()); user.setToken(token); userMapper.updateById(user); userInfo.getCurrentUserInfo().setToken(token); return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities()); } @Override public boolean supports(Class<?> aClass) { return true; } }
加密處理
import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.codec.Hex; import java.security.MessageDigest; @Slf4j public class PasswordUtils { /** * 校驗密碼是否一致 * * @param password: 前端傳過來的密碼 * @param hashedPassword:數據庫中儲存加密過后的密碼 * @param salt:鹽值 * @return */ public static boolean isValidPassword(String password, String hashedPassword, String salt) { return hashedPassword.equalsIgnoreCase(encodePassword(password, salt)); } /** * 通過SHA1對密碼進行編碼 * * @param password:密碼 * @param salt:鹽值 * @return */ public static String encodePassword(String password, String salt) { String encodedPassword; try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); if (salt != null) { digest.reset(); digest.update(salt.getBytes()); } byte[] hashed = digest.digest(password.getBytes()); int iterations = Constants.HASH_ITERATIONS - 1; for (int i = 0; i < iterations; ++i) { digest.reset(); hashed = digest.digest(hashed); } encodedPassword = new String(Hex.encode(hashed)); } catch (Exception e) { log.error("驗證密碼異常:", e); return null; } return encodedPassword; } }
安全認證用戶詳情
import cn.com.sercurity.cyy.user.entity.User; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; @Data @Slf4j public class SecurityUser implements UserDetails { /** * 當前登錄用戶 */ private transient User currentUserInfo; public SecurityUser() { } public SecurityUser(User user) { if (user != null) { this.currentUserInfo = user; } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin"); authorities.add(authority); return authorities; } @Override public String getPassword() { return currentUserInfo.getPassword(); } @Override public String getUsername() { return currentUserInfo.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
自定義userDetailsService - 認證用戶詳情
import cn.com.sercurity.cyy.config.security.dto.SecurityUser; import cn.com.sercurity.cyy.user.entity.User; import cn.com.sercurity.cyy.user.mapper.UserMapper; import com.baomidou.mybatisplus.mapper.EntityWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.List; @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; /*** * 根據賬號獲取用戶信息 * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 從數據庫中取出用戶信息 List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username)); User user; // 判斷用戶是否存在 if (!CollectionUtils.isEmpty(userList)){ user = userList.get(0); } else { throw new UsernameNotFoundException("用戶名不存在!"); } // 返回UserDetails實現類 return new SecurityUser(user); } }
import cn.com.sercurity.cyy.user.entity.User; import com.baomidou.mybatisplus.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; /** * * <p> 系統管理-用戶基礎信息表 Mapper 接口 </p> **/ @Mapper public interface UserMapper extends BaseMapper<User> { }
前端頁面
這里2個簡單的html頁面模擬前后端分離情況下登陸處理場景
1、登陸頁
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Login</title> </head> <body> <h1>Spring Security</h1> <form method="post" action="" onsubmit="return false"> <div> 用戶名:<input type="text" name="username" id="username"> </div> <div> 密碼:<input type="password" name="password" id="password"> </div> <div> <!-- <label><input type="checkbox" name="remember-me" id="remember-me"/>自動登錄</label>--> <button onclick="login()">登陸</button> </div> </form> </body> <script src="http://libs.baidu.com/jquery/1.9.0/jquery.js" type="text/javascript"></script> <script type="text/javascript"> function login() { var username = document.getElementById("username").value; var password = document.getElementById("password").value; // var rememberMe = document.getElementById("remember-me").value; $.ajax({ async: false, type: "POST", dataType: "json", url: '/login', contentType: "application/json", data: JSON.stringify({ "username": username, "password": password // "remember-me": rememberMe }), success: function (result) { console.log(result) if (result.code == 200) { alert("登陸成功"); window.location.href = "../home.html"; } else { alert(result.message) } } }); } </script> </html>
2、首頁
home.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h3>您好,登陸成功</h3> <button onclick="window.location.href='/logout'">退出登錄</button> </body> </html>
測試接口
import cn.com.sercurity.cyy.common.dto.ApiResult; import cn.com.sercurity.cyy.common.util.ResponseUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j @RestController public class IndexController { @GetMapping("/") public ModelAndView showHome() { return new ModelAndView("home.html"); } @GetMapping("/index") public String index() { return "Hello World ~"; } @GetMapping("/login") public ModelAndView login() { return new ModelAndView("login.html"); } @GetMapping("/home") public String home() { String name = SecurityContextHolder.getContext().getAuthentication().getName(); log.info("登陸人:" + name); return "Hello~ " + name; } @GetMapping(value ="/admin") // 訪問路徑`/admin` 具有`crud`權限 @PreAuthorize("hasPermission('/admin','crud')") public String admin() { return "Hello~ 管理員"; } @GetMapping("/test") // @PreAuthorize("hasPermission('/test','t')") public String test() { return "Hello~ 測試權限訪問接口"; } /** * 登錄異常處理 - 前后端一體的情況下 * @param request * @param response */ @RequestMapping("/login/error") public void loginError(HttpServletRequest request, HttpServletResponse response) { AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION"); log.error(e.getMessage()); ResponseUtils.out(response, ApiResult.fail(e.getMessage())); } }
使用response輸出JSON
import cn.com.sercurity.cyy.common.dto.ApiResult; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Slf4j public class ResponseUtils { /** * 使用response輸出JSON * * @param response * @param result */ public static void out(ServletResponse response, ApiResult result) { PrintWriter out = null; try { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); out = response.getWriter(); out.println(JSON.toJSONString(result)); } catch (Exception e) { log.error(e + "輸出JSON出錯"); } finally { if (out != null) { out.flush(); out.close(); } } } /** * 響應內容 * @param httpServletResponse * @param msg * @param status */ public static void getResponse(HttpServletResponse httpServletResponse, String msg, Integer status){ PrintWriter writer = null; httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); try { writer = httpServletResponse.getWriter(); writer.print(JSONObject.toJSONString(new ApiResult(status,msg,null))); } catch (IOException e) { log.error("響應報錯", e.getMessage()); } finally { if (writer != null){ writer.close(); } } } }
測試訪問效果
數據庫賬號:admin 密碼:123456
1. 輸入錯誤用戶名提示該用戶不存在
2. 輸入錯誤密碼提示密碼錯誤
3. 輸入正確用戶名和賬號,提示登陸成功,然后跳轉到首頁
登陸成功后即可正常訪問其他接口,如果是未登錄情況下將訪問不了
總結
- 在
Spring Security核心配置類
中設置自定義的用戶密碼校驗過濾器(AdminAuthenticationProcessingFilter)
- 在自定義的用戶密碼校驗過濾器中配置
認證管理器(CusAuthenticationManager)
、認證成功處理(AdminAuthenticationSuccessHandler)
和認證失敗處理(AdminAuthenticationFailureHandler)
等 - 在自定義的認證管理器中配置自定義的
認證處理(AdminAuthenticationProvider)
- 然后就是在認證處理中實現自己的相應業務邏輯等
如果缺少什么了,歡迎大家留言,我會及時補充的