springsecurity?登錄認證流程分析一(ajax)
一、準備工作
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坐標 --> <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坐標--> <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>
二、認證
2.1 登錄認證流程
接口解釋
Authentication接口: 它的實現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息;
AuthenticationManager接口:定義了認證Authentication的方法;
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的 方法;
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝 成UserDetails對象返回。然后將這些信息封裝到Authentication對象中;
2.3 自定義數(shù)據(jù)源分析
①自定義登錄接口 調(diào)用ProviderManager的方法進行認證 如果認證通過生成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; //返回的成功或者失敗的標識符 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; } //使用下面四個方法,方面以后使用鏈式編程 // 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 認證的實現(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è)置默認為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 方法,你可以指定哪些請求路徑不 需要進行身份驗證。
http.authorizeRequests() 可以配置請求的授權(quán)規(guī)則。 例 如, .anyRequest().authenticated() 表示任何請求都需要經(jīng)過身份驗證。 http.requestMatchers 表示某個請求不需要進行身份校驗,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/"); } /** * 配置默認首頁地址 * @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 認證 */ @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)前請求不是登錄請求,則需要進行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ù)中進行獲取 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的方法進行認證 如果認證通過生成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)用進行校驗 Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); //3、如果authenticate為空 if(ObjectUtils.isEmpty(authenticate)){ throw new CustomerAuthenticationException("登錄失??!"); } //放入的用戶信息 LoginSessionUserInf loginSessionUserInf = (LoginSessionUserInf)authenticate.getPrincipal(); //生成jwt,將用戶名+uuid 放進去 這樣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;">歡迎進入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: '登錄成功,稍后進行跳轉(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 >歡迎進入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 登錄認證一(ajax)的文章就介紹到這了,更多相關(guān)springsecurity 登錄認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java插入排序和希爾排序?qū)崿F(xiàn)思路及代碼
這篇文章主要介紹了插入排序和希爾排序兩種排序算法,文章通過代碼示例和圖解詳細介紹了這兩種排序算法的實現(xiàn)過程和原理,需要的朋友可以參考下2025-03-03在Java 8中將List轉(zhuǎn)換為Map對象方法
這篇文章主要介紹了在Java 8中將List轉(zhuǎn)換為Map對象方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-11-11idea左側(cè)的commit框設(shè)置顯示出來方式
在IDEA中顯示左側(cè)的commit框,首先通過File-Settings-Version Control-Commit進行設(shè)置,然后勾選Use non-modal commit interface完成2025-01-01Java使用ProcessBuilder?API優(yōu)化流程
Java?的?Process?API?為開發(fā)者提供了執(zhí)行操作系統(tǒng)命令的強大功能,這篇文章將詳細介紹如何使用?ProcessBuilder?API?來方便的操作系統(tǒng)命令,需要的可以收藏一下2023-06-06