springsecurity?登錄認(rèn)證流程分析一(ajax)
一、準(zhǔn)備工作
1.1 導(dǎo)入依賴
因springboot 3.0 + 以上版本只能支持java17 顧使用2.5.0 版本


<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<!-- <version>2.7.18</version>-->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- thymeleaf 相關(guān)依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<!-- mybatis坐標(biāo) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <version>8.0.28</version>-->
</dependency>
<!--validation依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--redis坐標(biāo)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--springdoc-openapi-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.1.0</version>
</dependency>
<!--fastjson依賴-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>二、認(rèn)證
2.1 登錄認(rèn)證流程

接口解釋
Authentication接口: 它的實現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息;
AuthenticationManager接口:定義了認(rèn)證Authentication的方法;
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的 方法;
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝 成UserDetails對象返回。然后將這些信息封裝到Authentication對象中;
2.3 自定義數(shù)據(jù)源分析

①自定義登錄接口 調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成jwt 把用戶信息存入redis中;
②自定義UserDetailsService 在這個實現(xiàn)類中去查詢數(shù)據(jù)庫;
2.4 自定義數(shù)據(jù)源查詢代碼實現(xiàn)(可實現(xiàn)多數(shù)據(jù)源模式,db2,mysql)
2.4.1 自定義數(shù)據(jù)源掃描mapper
package com.fashion.config.datasource;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* @Author: LQ
* @Date 2024/8/17 14:23
* @Description: mysql 配置
*/
@Configuration
@MapperScan(basePackages = "com.fashion.mapper.mysql",sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MysqlDataSourceConfig {
@Primary
@Bean
public DataSource mysqlDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/lq");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Primary
@Bean
public SqlSessionFactory mysqlSqlSessionFactory(@Autowired DataSource mysqlDataSource){
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(mysqlDataSource);
sessionFactory.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.xml"));
try {
// mapper xml 文件位置
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mybatis/mapper/mysql/*.xml"));
// sessionFactory.setMapperLocations(new ClassPathResource("/mybatis/mapper/mysql/*.xml"));
return sessionFactory.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}2.4.2 自定義 UserDetailsService
package com.fashion.service;
import com.fashion.domain.LoginSessionUserInf;
import com.fashion.domain.mysql.TUserInf;
import com.fashion.exception.CustomerAuthenticationException;
import com.fashion.mapper.mysql.TUserInfMapper;
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.Component;
import org.springframework.util.ObjectUtils;
import java.util.Arrays;
import java.util.List;
/**
* @Author: LQ
* @Date 2024/8/13 21:12
* @Description:
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private TUserInfMapper userInfMapper;
@Override
public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
// 根據(jù)用戶名獲取用戶信息
if (ObjectUtils.isEmpty(loginId)) {
throw new CustomerAuthenticationException("用戶名不能為空!");
}
TUserInf tUserInf = userInfMapper.selectByLoginId(loginId);
if (ObjectUtils.isEmpty(tUserInf)) {
throw new CustomerAuthenticationException("用戶不存在!");
}
// 獲取權(quán)限信息 todo:后期從數(shù)據(jù)庫查詢
List<String> perList = Arrays.asList("new:query", "news:delete");
LoginSessionUserInf loginSessionUserInf = new LoginSessionUserInf(tUserInf, perList);
return loginSessionUserInf;
}
}2.4.3 自定義 UserDetails
package com.fashion.domain;
import com.alibaba.fastjson.annotation.JSONField;
import com.fashion.domain.mysql.TUserInf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: LQ
* @Date 2024/8/17 15:57
* @Description: 用戶登錄信息
*/
@Data
public class LoginSessionUserInf implements UserDetails {
private TUserInf userInf;
public LoginSessionUserInf() {
}
@JsonIgnore
@JSONField(serialize=false)
private List<GrantedAuthority> grantedAuthorities;
// 權(quán)限列表
private List<String> perList;
public LoginSessionUserInf(TUserInf userInf, List<String> perList) {
this.userInf = userInf;
this.perList = perList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (grantedAuthorities != null) {
return grantedAuthorities;
}
grantedAuthorities = perList.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return grantedAuthorities;
}
@Override
public String getPassword() {
return userInf.getLoginPwd();
}
@Override
public String getUsername() {
return userInf.getLoginId();
}
//判斷賬號是否未過期
@Override
public boolean isAccountNonExpired() {
return "1".equals(userInf.getStatus());
}
//判斷賬號是否沒有鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
//判斷賬號是否沒有超時
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//判斷賬號是否可用
@Override
public boolean isEnabled() {
return true;
}
}2.4.4 創(chuàng)建用戶sql
create table t_user_inf(
id int primary key auto_increment comment '主鍵id',
login_id varchar(64) default '' comment '登錄賬號id',
login_pwd varchar(128) default '' comment '登錄密碼',
user_nm varchar(126) default '' comment '登錄賬號名稱',
status varchar(2) default '1' comment '狀態(tài) 1正常',
phone varchar(11) default '' comment '手機號',
source_type varchar(2) default '1' comment '登錄來源 1 賬密 2 githup',
address varchar(128) default '' comment '家庭住址',
cre_date datetime default now() comment '創(chuàng)建時間',
upd_date datetime default now() comment '更新時間',
upd_usr varchar(64) default '' comment '更新人'
);2.4.5 其他實體類(用戶類)
package com.fashion.domain.mysql;
import java.util.Date;
import lombok.Data;
@Data
public class TUserInf {
/**
* 主鍵id
*/
private Integer id;
/**
* 登錄賬號id
*/
private String loginId;
/**
* 登錄密碼
*/
private String loginPwd;
/**
* 登錄賬號名稱
*/
private String userNm;
/**
* 狀態(tài) 1正常
*/
private String status;
/**
* 手機號
*/
private String phone;
/**
* 登錄來源 1 賬密 2 githup
*/
private String sourceType;
/**
* 家庭住址
*/
private String address;
/**
* 創(chuàng)建時間
*/
private Date creDate;
/**
* 更新時間
*/
private Date updDate;
/**
* 更新人
*/
private String updUsr;
}2.4.6 通用返回類
package com.fashion.domain;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: LQ
* @Date 2024/8/17 15:08
* @Description:
*/
@Data
public class R {
private Boolean success; //返回的成功或者失敗的標(biāo)識符
private Integer code; //返回的狀態(tài)碼
private String message; //提示信息
private Map<String, Object> data = new HashMap<String, Object>(); //數(shù)據(jù)
//把構(gòu)造方法私有
private R() {}
//成功的靜態(tài)方法
public static R ok(){
R r=new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
return r;
}
//失敗的靜態(tài)方法
public static R error(){
R r=new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失敗");
return r;
}
//使用下面四個方法,方面以后使用鏈?zhǔn)骄幊?
// R.ok().success(true)
// r.message("ok).data("item",list)
public R success(Boolean success){
this.setSuccess(success);
return this; //當(dāng)前對象 R.success(true).message("操作成功").code().data()
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}2.5 配置類/工具類
package com.fashion.utils;
import cn.hutool.core.util.IdUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
* @Author: LQ
* @Date 2024/8/17 15:38
* @Description: jwt 工具類
*/
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一個小時
//設(shè)置秘鑰明文(鹽)
public static final String JWT_KEY = "LQlacd";
//生成令牌
public static String getUUID(){
String token = IdUtil.fastSimpleUUID();
return token;
}
/**
* 生成jtw
* @param subject token中要存放的數(shù)據(jù)(json格式) 用戶數(shù)據(jù)
* @param ttlMillis token超時時間
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設(shè)置
//過期時間
return builder.compact();
}
//生成jwt的業(yè)務(wù)邏輯代碼
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,
String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();//獲取到系統(tǒng)當(dāng)前的時間戳
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主題 可以是JSON數(shù)據(jù)
.setIssuer("xx") // 簽發(fā)者
.setIssuedAt(now) // 簽發(fā)時間
.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數(shù)為秘鑰
.setExpiration(expDate);
}
/**
* 創(chuàng)建token
* @param id
* @param subject
* @param ttlMillis
添加依賴
2.3.5 認(rèn)證的實現(xiàn)
1 配置數(shù)據(jù)庫校驗登錄用戶
從之前的分析我們可以知道,我們可以自定義一個UserDetailsService,讓SpringSecurity使用我們的
UserDetailsService。我們自己的UserDetailsService可以從數(shù)據(jù)庫中查詢用戶名和密碼。
我們先創(chuàng)建一個用戶表, 建表語句如下:
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設(shè)置過期時間
return builder.compact();
}
/**
* 生成加密后的秘鑰 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length,
"AES");
return key;
}
/**
* 解析jwt
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}2.5.1 webUtild 工具類
package com.fashion.utils;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.nio.charset.StandardCharsets;
/**
* @Author: LQ
* @Date 2024/8/17 16:56
* @Description:
*/
@Slf4j
public class WebUtils {
/**
* 寫內(nèi)容到客戶端
* @param response
* @param obj
*/
public static void writeResp(HttpServletResponse response,Object obj) {
try {
//設(shè)置客戶端的響應(yīng)的內(nèi)容類型
response.setContentType("application/json;charset=UTF-8");
//獲取輸出流
ServletOutputStream outputStream = response.getOutputStream();
//消除循環(huán)引用
String result = JSONUtil.toJsonStr(obj);
SerializerFeature.DisableCircularReferenceDetect);
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("寫出字符流失敗",e);
}
}
}2.5.2 redis 工具類配置
package com.fashion.config.datasource;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.client.RestTemplate;
/**
* @Author: LQ
* @Date 2024/8/17 15:18
* @Description:
*/
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lettuceConnectionFactory =
new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1", 6379));
return lettuceConnectionFactory;
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setHashKeySerializer(jackson2JsonRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
/**
* redis 值序列化方式
* @return
*/
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 自動檢測所有類的全部屬性
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) ;
// 此項必須配置,否則會報java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// 此設(shè)置默認(rèn)為true,就是在反序列化遇到未知屬性時拋異常,這里設(shè)置為false,目的為忽略部分序列化對象存入緩存時誤存的其他方法的返回值
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}2.5.3 spring security 配置
HttpSecurity參數(shù)說明 SecurityFilterChain : 一個表示安全過濾器鏈的對象 http.antMatchers(...).permitAll() 通過 antMatchers 方法,你可以指定哪些請求路徑不 需要進(jìn)行身份驗證。
http.authorizeRequests() 可以配置請求的授權(quán)規(guī)則。 例 如, .anyRequest().authenticated() 表示任何請求都需要經(jīng)過身份驗證。 http.requestMatchers 表示某個請求不需要進(jìn)行身份校驗,permitAll 隨意訪問。 http.httpBasic() 配置基本的 HTTP 身份驗證。 http.csrf() 通過 csrf 方法配置 CSRF 保護。 http.sessionManagement() 不會創(chuàng)建會話。這意味著每個請求都是獨立的,不依賴于之前的 請求。適用于 RESTful 風(fēng)格的應(yīng)用。
package com.fashion.config;
import com.fashion.filter.ImgVerifyFilter;
import com.fashion.filter.JwtAuthenticationTokenFilter;
import com.fashion.handler.AnonymousAuthenticationHandler;
import com.fashion.handler.CustomerAccessDeniedHandler;
import com.fashion.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Arrays;
import java.util.List;
/**
* @Author: LQ
* @Date 2024/8/13 21:12
* @Description:
*/
@Configuration
public class SecurityFilterConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private ImgVerifyFilter imgVerifyFilter;
@Autowired
private AuthenticationFailureHandler loginFailureHandler;
// @Autowired
// private LoginSuccessHandler loginSuccessHandler;
@Autowired
private CustomerAccessDeniedHandler customerAccessDeniedHandler;
@Autowired
private AnonymousAuthenticationHandler anonymousAuthenticationHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private static List<String> EXCLUDE_URL_LIST = Arrays.asList("/static/**","/user/**","/comm/**","/","/favicon.ico");
/**
* 登錄時需要調(diào)用AuthenticationManager.authenticate執(zhí)行一次校驗
*
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// 入口配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 關(guān)閉crsf
http.csrf(csrf -> csrf.disable());
// 放行靜態(tài)資源,以及登錄接口放行
http.authorizeRequests().antMatchers(EXCLUDE_URL_LIST.toArray(new String[]{}))
.permitAll()
.anyRequest().authenticated();
// 設(shè)置數(shù)據(jù)源
http.userDetailsService(userDetailsService);
// 配置異常過濾器
//http.formLogin().failureHandler(loginFailureHandler);
// 其他異常處理
http.exceptionHandling(config ->
{
config.accessDeniedHandler(customerAccessDeniedHandler);
config.authenticationEntryPoint(anonymousAuthenticationHandler);
}
);
// 添加圖形驗證碼過濾器
http.addFilterBefore(imgVerifyFilter, UsernamePasswordAuthenticationFilter.class);
// jwt token 校驗
http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}2.5.4 web 配置靜態(tài)資源放行等信息
package com.fashion.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author: LQ
* @Date 2024/8/17 16:32
* @Description:
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 放行靜態(tài)資源
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
/**
* 配置默認(rèn)首頁地址
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOrigins("*")
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .allowedHeaders("*")
// .allowCredentials(true);
// }
}2.5.5 異常類編寫
/**
* @Author: LQ
* @Date 2024/8/17 20:29
* @Description:
*/
public class CustomerAccessException extends AccessDeniedException {
public CustomerAccessException(String msg) {
super(msg);
}
}
/**
* @Author: LQ
* @Date 2024/8/17 15:35
* @Description: 無權(quán)限資源時異常
*/
public class CustomerAuthenticationException extends AuthenticationException {
public CustomerAuthenticationException(String msg) {
super(msg);
}
}2.5.6 過濾器(圖形驗證碼過濾器)
package com.fashion.filter;
import com.fashion.constants.ComConstants;
import com.fashion.domain.R;
import com.fashion.handler.AnonymousAuthenticationHandler;
import com.fashion.utils.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: LQ
* @Date 2024/8/17 19:29
* @Description: 圖像驗證碼過濾器
*/
@Component
@Slf4j
public class ImgVerifyFilter extends OncePerRequestFilter {
@Autowired
private HttpServletRequest request;
@Autowired
private AnonymousAuthenticationHandler anonymousAuthenticationHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String reqUrl = httpServletRequest.getRequestURI();
log.info("請求url:{}",reqUrl);
if (ComConstants.LOGIN_URL.equals(reqUrl)) {
// 開始校驗圖形驗證碼
Object imgCode = request.getParameter("imageCode");
Object sessCode = request.getSession().getAttribute(ComConstants.SESSION_IMAGE);
// 判斷是否和庫里面相等
log.info("傳過來的驗證碼為:{},session中的為:{}",imgCode,sessCode);
if (!sessCode.equals(imgCode)) {
//throw new CustomerAuthenticationException("圖像驗證碼錯誤");
WebUtils.writeResp(httpServletResponse, R.error().code(400).message("圖像驗證碼失??!"));
return;
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}2.5.7 jwt 過濾器
作用:因為禁用了session所以需要將 SecurityContextHolder.getContext() 中
package com.fashion.filter;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.fashion.constants.ComConstants;
import com.fashion.constants.RedisPreConst;
import com.fashion.domain.JwtToken;
import com.fashion.domain.LoginSessionUserInf;
import com.fashion.exception.CustomerAuthenticationException;
import com.fashion.handler.LoginFailureHandler;
import com.fashion.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: LQ
* @Date 2024/8/17 22:12
* @Description: jwt 認(rèn)證
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
//獲取當(dāng)前請求的url地址
String url = request.getRequestURI();
//如果當(dāng)前請求不是登錄請求,則需要進(jìn)行token驗證
if (!url.equals(ComConstants.LOGIN_URL) && !url.startsWith("/user/") && !url.startsWith("/comm")
&& !url.equals("/") && !url.startsWith("/favicon.ico") && !url.endsWith("js") && !url.endsWith("map")) {
this.validateToken(request);
}
} catch (AuthenticationException e) {
log.error("jwt異常");
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
//登錄請求不需要驗證token
doFilter(request, response, filterChain);
}
/**
* 校驗token有效性
* @param request
* @throws AuthenticationException
*/
private void validateToken(HttpServletRequest request) throws
AuthenticationException {
//從頭部獲取token信息
String token = request.getHeader("token");
//如果請求頭部沒有獲取到token,則從請求的參數(shù)中進(jìn)行獲取
if (ObjectUtils.isEmpty(token)) {
token = request.getParameter("token");
}
if (ObjectUtils.isEmpty(token)) {
throw new CustomerAuthenticationException("token不存在");
}
//如果存在token,則從token中解析出用戶名
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
throw new CustomerAuthenticationException("token解析失敗");
}
//獲取到主題
String loginUserString = claims.getSubject();
//把字符串轉(zhuǎn)成loginUser對象
JwtToken jwtToken = JSON.parseObject(loginUserString, JwtToken.class);
// 拿到中間的uuid去庫里面得到用戶信息
String userTokenPre = String.format(RedisPreConst.LOGIN_TOKEN,jwtToken.getToken());
// 將用戶信息放到redis中 24小時后過期
String redisUser = stringRedisTemplate.opsForValue().get(userTokenPre);
if (ObjectUtils.isEmpty(redisUser)) {
throw new CustomerAuthenticationException("用戶信息過期,請重新登錄!");
}
LoginSessionUserInf loginUser = JSONUtil.toBean(redisUser,LoginSessionUserInf.class);
//創(chuàng)建身份驗證對象
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(loginUser, null,
loginUser.getAuthorities());
//設(shè)置到Spring Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}2.6 自定義登錄接口
2.6.1 登錄controller 接口
package com.fashion.controller;
import com.fashion.domain.R;
import com.fashion.domain.req.LoginUserReq;
import com.fashion.service.UserLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: LQ
* @Date 2024/8/17 16:05
* @Description: 用戶登錄接口
*/
@RestController
@RequestMapping("user/")
public class UserLoginController {
@Autowired
private UserLoginService userLoginService;
/**
* 用戶登錄
* @param req
* @return
*/
@RequestMapping("login")
public R userLogin(LoginUserReq req) {
return userLoginService.login(req);
}
}2.6.2 UserLoginService 用戶自定義接口
package com.fashion.service;
import com.fashion.domain.R;
import com.fashion.domain.req.LoginUserReq;
/**
* @Author: LQ
* @Date 2024/8/17 16:07
* @Description: 用戶自定義登錄重寫 ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成jw
*/
public interface UserLoginService {
/**
* 登錄
* @param userInf
* @return
*/
R login(LoginUserReq userInf);
}
@Service
@Slf4j
public class UserLoginServiceImpl implements UserLoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public R login(LoginUserReq userInf) {
// 1 封裝 authenticationToken 對象,密碼校驗等信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userInf.getLoginId(),userInf.getLoginPwd());
// 2 開始調(diào)用進(jìn)行校驗
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//3、如果authenticate為空
if(ObjectUtils.isEmpty(authenticate)){
throw new CustomerAuthenticationException("登錄失??!");
}
//放入的用戶信息
LoginSessionUserInf loginSessionUserInf = (LoginSessionUserInf)authenticate.getPrincipal();
//生成jwt,將用戶名+uuid 放進(jìn)去 這樣jwt 就比較小,更好校驗,將token 作為key 把loginsesionUser信息放到redis中
JwtToken jwtToken = new JwtToken();
jwtToken.setLoginId(loginSessionUserInf.getUsername());
jwtToken.setToken(JwtUtil.getUUID());
String loginUserString = JSONUtil.toJsonStr(jwtToken);
//調(diào)用JWT工具類,生成jwt令牌
String jwtStr = JwtUtil.createJWT(jwtToken.getToken(), loginUserString, JwtUtil.JWT_TTL);
log.info("jwt token 生成成功:{}",jwtStr);
String userTokenPre = String.format(RedisPreConst.LOGIN_TOKEN,jwtToken.getToken());
log.info("用戶拼接后的前綴信息:{}",userTokenPre);
// 將用戶信息放到redis中 24小時后過期
stringRedisTemplate.opsForValue().set(userTokenPre, JSONObject.toJSONString(loginSessionUserInf),24, TimeUnit.HOURS);
// 跳轉(zhuǎn)到頁面
return R.ok().data("token",jwtStr).message("/main/index");
}
}2.6.3 代碼截圖

2.6.4 驗證碼controller
package com.fashion.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.generator.RandomGenerator;
import com.fashion.constants.ComConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.IOException;
/**
* @Author: LQ
* @Date 2024/8/17 16:05
* @Description: 通用接口,不用攔截
*/
@Controller
@RequestMapping("comm/")
@Slf4j
public class ComController {
@Autowired
private HttpServletRequest request;
/**
* 獲取圖像驗證碼
* @param response
*/
@RequestMapping("getVerifyImage")
public void getVerifyImage(HttpServletResponse response) {
RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
//定義圖形驗證碼的長、寬、驗證碼位數(shù)、干擾線數(shù)量
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(120, 40,4,19);
lineCaptcha.setGenerator(randomGenerator);
lineCaptcha.createCode();
//設(shè)置背景顏色
lineCaptcha.setBackground(new Color(249, 251, 220));
//生成四位驗證碼
String code = lineCaptcha.getCode();
log.info("圖形驗證碼生成成功:{}",code);
request.getSession().setAttribute(ComConstants.SESSION_IMAGE,code);
response.setContentType("image/jpeg");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
try {
lineCaptcha.write(response.getOutputStream());
} catch (IOException e) {
log.error("圖像驗證碼獲取失?。?,e);
}
}
}2.6.5 登錄首頁
package com.fashion.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Author: LQ
* @Date 2024/8/17 22:06
* @Description: main的主頁
*/
@Controller
@RequestMapping("main/")
@Slf4j
public class MainController {
@RequestMapping("index")
public String index() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
log.info("我來首頁了,用戶信息:{}",principal);
return "main";
}
}2.7 前端頁面
2.7.1 前端效果


2.7.2 前端代碼
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄頁</title>
<!-- 引入樣式 -->
<link rel="stylesheet" rel="external nofollow" rel="external nofollow" >
<style type="text/css">
#app{width: 600px;margin: 28px auto 10px }
img{cursor: pointer;}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h2 style="margin-left: 140px;">歡迎進(jìn)入springsecurity</h2>
</el-header>
<el-main>
<el-form ref="form" :model="form" label-width="140px" :rules="rules">
<el-form-item label="用戶名" prop="loginId">
<el-input v-model="form.loginId" ></el-input>
</el-form-item>
<el-form-item label="登錄密碼" prop="loginPwd">
<el-input v-model="form.loginPwd"></el-input>
</el-form-item>
<el-form-item label="圖像驗證碼" prop="imageCode">
<el-col :span="10">
<el-input v-model="form.imageCode"></el-input>
</el-col>
<!--<el-col class="line" :span="4"></el-col>-->
<el-col :span="5" :offset="1">
<img :src="form.imageCodeUrl" @click="getVerifyCode">
</el-col>
</el-form-item>
<!-- <el-form-item label="即時配送">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>-->
<el-form-item>
<el-button type="primary" :loading="status.loading" @click="onSubmit('form')" style="width: 400px;">登錄</el-button>
<!-- <el-button>取消</el-button>-->
</el-form-item>
</el-form>
</el-main>
<!-- <el-footer>Footer</el-footer>-->
</el-container>
</div>
<script type="text/javascript" th:src="@{/static/js/axios.js}"></script>
<script type="text/javascript" th:src="@{/static/js/vue2.js }"></script>
<!-- 引入組件庫 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script type="text/javascript">
var app = new Vue({
el:"#app",
data:{
form: {
loginId: 'admin',
loginPwd: '12345678',
imageCode: '1111',
imageCodeUrl: '/comm/getVerifyImage'
}
,status: {
"loading": false
}
,
rules: {
loginId: [
{ required: true, message: '請?zhí)顚懙卿涃~號', trigger: 'blur' },
{ min: 3, max: 15, message: '長度在 3 到 15 個字符', trigger: 'blur' }
],
loginPwd: [
{ required: true, message: '請?zhí)顚懙卿浢艽a', trigger: 'blur' },
{ min: 3, max: 15, message: '長度在 3 到 15 個字符', trigger: 'blur' }
],
imageCode: [
{ required: true, message: '請?zhí)顚憟D像驗證碼', trigger: 'blur' },
{ min: 4, max: 4, message: '長度在4個', trigger: 'blur' }
],
}
}
,methods:{
onSubmit:function(formName) {
let that = this;
that.status.loading = true;
this.$refs[formName].validate((valid) => {
if (valid) {
let forData = JSON.stringify(that.form);
let formData = new FormData();
formData.append('loginId', that.form.loginId);
formData.append('loginPwd', that.form.loginPwd);
formData.append('imageCode', that.form.imageCode);
//console.log(forData);
axios.post("/user/login",
formData
)
.then(function (response) {
let resData = response.data;
console.log(resData);
that.status.loading = false;
if (resData.code != '0000') {
that.$message.error(resData.message);
// 刷新驗證碼
that.getVerifyCode();
} else {
that.$message({
showClose: true,
message: '登錄成功,稍后進(jìn)行跳轉(zhuǎn)',
type: 'success'
});
let url = resData.message + "?token=" + resData.data.token
window.location.href = url;
}
})
} else {
that.$message.error('請完整填寫信息');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
,getVerifyCode: function () {
console.log("getVerifyCode")
this.form.imageCodeUrl = '/comm/getVerifyImage?v='+new Date();
}
}
});
</script>
</body>
</html>2.7.3 登錄成功頁面

2.7.4 htm 代碼
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>主頁菜單</title>
<!-- 引入樣式 -->
<link rel="stylesheet" rel="external nofollow" rel="external nofollow" >
<style type="text/css">
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h2 >歡迎進(jìn)入springsecurity 配置主頁</h2>
</el-header>
<el-container>
<el-aside width="400px">
<el-row class="tac">
<el-col :span="12">
<h5>菜單</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>導(dǎo)航一</span>
</template>
<el-menu-item-group>
<!-- <template slot="title">分組一</template>-->
<el-menu-item index="1-1">選項1</el-menu-item>
<el-menu-item index="1-2">選項2</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">導(dǎo)航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">導(dǎo)航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">導(dǎo)航四</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</el-aside>
<el-main>我是內(nèi)容</el-main>
</el-container>
<!-- <el-footer>Footer</el-footer>-->
</el-container>
</div>
<script type="text/javascript" th:src="@{/static/js/axios.js}"></script>
<script type="text/javascript" th:src="@{/static/js/vue2.js }"></script>
<!-- 引入組件庫 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script type="text/javascript">
var app = new Vue({
el:"#app",
data:{
}
,methods:{
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
}
}
});
</script>
</body>
</html>到此這篇關(guān)于springsecurity 登錄認(rèn)證一(ajax)的文章就介紹到這了,更多相關(guān)springsecurity 登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springsecurity實現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項目)
- Springboot整合SpringSecurity實現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- SpringSecurity?默認(rèn)登錄認(rèn)證的實現(xiàn)原理解析
- SpringSecurity實現(xiàn)前后端分離登錄token認(rèn)證詳解
- Java SpringSecurity+JWT實現(xiàn)登錄認(rèn)證
- SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實現(xiàn)
- SpringBoot整合SpringSecurity和JWT的示例
相關(guān)文章
java插入排序和希爾排序?qū)崿F(xiàn)思路及代碼
這篇文章主要介紹了插入排序和希爾排序兩種排序算法,文章通過代碼示例和圖解詳細(xì)介紹了這兩種排序算法的實現(xiàn)過程和原理,需要的朋友可以參考下2025-03-03
在Java 8中將List轉(zhuǎn)換為Map對象方法
這篇文章主要介紹了在Java 8中將List轉(zhuǎn)換為Map對象方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-11-11
idea左側(cè)的commit框設(shè)置顯示出來方式
在IDEA中顯示左側(cè)的commit框,首先通過File-Settings-Version Control-Commit進(jìn)行設(shè)置,然后勾選Use non-modal commit interface完成2025-01-01
Java使用ProcessBuilder?API優(yōu)化流程
Java?的?Process?API?為開發(fā)者提供了執(zhí)行操作系統(tǒng)命令的強大功能,這篇文章將詳細(xì)介紹如何使用?ProcessBuilder?API?來方便的操作系統(tǒng)命令,需要的可以收藏一下2023-06-06

