springsecurity 登录认证一(ajax)

一、准备工作

        1.1 导入依赖

  因springboot 3.0 + 以上版本只能支持java17 顾使用2.5.0 版本 

ccbeefb65915450ab5d5c8120181bcc2.png

dd5e902bdd0349afa9b846999d2d214b.png

  <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 相关依赖 --><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 登录认证流程

54c0811aeadb4ec7a59ae22a26dff4e2.png

接口解释

        Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息;

        AuthenticationManager接口:定义了认证Authentication的方法;

        UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的 方法;

      UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装 成UserDetails对象返回。然后将这些信息封装到Authentication对象中;  

2.3  自定义数据源分析

11932b7c43d74f7c96acd4e781385ee2.png

①自定义登录接口 调用ProviderManager的方法进行认证 如果认证通过生成jwt 把用户信息存入redis中;

②自定义UserDetailsService 在这个实现类中去查询数据库;

2.4  自定义数据源查询代码实现(可实现多数据源模式,db2,mysql)

 2.4.1 自定义数据源扫描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@Beanpublic 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@Beanpublic 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 {@Autowiredprivate TUserInfMapper userInfMapper;@Overridepublic UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {// 根据用户名获取用户信息if (ObjectUtils.isEmpty(loginId)) {throw new CustomerAuthenticationException("用户名不能为空!");}TUserInf tUserInf = userInfMapper.selectByLoginId(loginId);if (ObjectUtils.isEmpty(tUserInf)) {throw new CustomerAuthenticationException("用户不存在!");}// 获取权限信息 todo:后期从数据库查询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;// 权限列表private List<String> perList;public LoginSessionUserInf(TUserInf userInf, List<String> perList) {this.userInf = userInf;this.perList = perList;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (grantedAuthorities != null) {return grantedAuthorities;}grantedAuthorities = perList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return grantedAuthorities;}@Overridepublic String getPassword() {return userInf.getLoginPwd();}@Overridepublic String getUsername() {return userInf.getLoginId();}//判断账号是否未过期@Overridepublic boolean isAccountNonExpired() {return "1".equals(userInf.getStatus());}//判断账号是否没有锁定@Overridepublic boolean isAccountNonLocked() {return true;}//判断账号是否没有超时@Overridepublic boolean isCredentialsNonExpired() {return true;}//判断账号是否可用@Overridepublic boolean isEnabled() {return true;}
}

2.4.4 创建用户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 '状态 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 '创建时间',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;/*** 状态 1正常*/private String status;/*** 手机号*/private String phone;/*** 登录来源 1 账密  2 githup*/private String sourceType;/*** 家庭住址*/private String address;/*** 创建时间*/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; //返回的状态码private String message; //提示信息private Map<String, Object> data = new HashMap<String, Object>(); //数据//把构造方法私有private R() {}//成功的静态方法public static R ok(){R r=new R();r.setSuccess(true);r.setCode(ResultCode.SUCCESS);r.setMessage("成功");return r;}//失败的静态方法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; //当前对象 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 一个小时//设置秘钥明文(盐)public static final String JWT_KEY = "LQlacd";//生成令牌public static String getUUID(){String token = IdUtil.fastSimpleUUID();return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式) 用户数据* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置//过期时间return builder.compact();}//生成jwt的业务逻辑代码private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();//获取到系统当前的时间戳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数据.setIssuer("xx") // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis添加依赖2.3.5 认证的实现1 配置数据库校验登录用户从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。我们先创建一个用户表, 建表语句如下:* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间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 {/***  写内容到客户端* @param response* @param obj*/public static void writeResp(HttpServletResponse response,Object obj) {try {//设置客户端的响应的内容类型response.setContentType("application/json;charset=UTF-8");//获取输出流ServletOutputStream outputStream = response.getOutputStream();//消除循环引用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 {@Beanpublic RedisConnectionFactory redisConnectionFactory() {LettuceConnectionFactory lettuceConnectionFactory =new LettuceConnectionFactory(new RedisStandaloneConfiguration("127.0.0.1", 6379));return lettuceConnectionFactory;}@Beanpublic 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;}@Beanpublic 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 XXXobjectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);// 此设置默认为true,就是在反序列化遇到未知属性时抛异常,这里设置为false,目的为忽略部分序列化对象存入缓存时误存的其他方法的返回值objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);return jackson2JsonRedisSerializer;}
}

2.5.3 spring security 配置

HttpSecurity参数说明 SecurityFilterChain : 一个表示安全过滤器链的对象 http.antMatchers(...).permitAll() 通过 antMatchers 方法,你可以指定哪些请求路径不 需要进行身份验证。

http.authorizeRequests() 可以配置请求的授权规则。 例 如, .anyRequest().authenticated() 表示任何请求都需要经过身份验证。 http.requestMatchers 表示某个请求不需要进行身份校验,permitAll 随意访问。 http.httpBasic() 配置基本的 HTTP 身份验证。 http.csrf() 通过 csrf 方法配置 CSRF 保护。 http.sessionManagement() 不会创建会话。这意味着每个请求都是独立的,不依赖于之前的 请求。适用于 RESTful 风格的应用。

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 {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate ImgVerifyFilter imgVerifyFilter;@Autowiredprivate AuthenticationFailureHandler loginFailureHandler;
//    @Autowired
//    private LoginSuccessHandler loginSuccessHandler;@Autowiredprivate CustomerAccessDeniedHandler customerAccessDeniedHandler;@Autowiredprivate AnonymousAuthenticationHandler anonymousAuthenticationHandler;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;private static List<String> EXCLUDE_URL_LIST = Arrays.asList("/static/**","/user/**","/comm/**","/","/favicon.ico");/*** 登录时需要调用AuthenticationManager.authenticate执行一次校验**/@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}// 入口配置@Overrideprotected void configure(HttpSecurity http) throws Exception {// 关闭crsfhttp.csrf(csrf -> csrf.disable());// 放行静态资源,以及登录接口放行http.authorizeRequests().antMatchers(EXCLUDE_URL_LIST.toArray(new String[]{})).permitAll().anyRequest().authenticated();// 设置数据源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);}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

2.5.4  web 配置静态资源放行等信息

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 {/***  放行静态资源* @param registry*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}/***  配置默认首页地址* @param registry*/@Overridepublic 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: 无权限资源时异常*/
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 {@Autowiredprivate HttpServletRequest request;@Autowiredprivate AnonymousAuthenticationHandler anonymousAuthenticationHandler;@Overrideprotected 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 {@Autowiredprivate LoginFailureHandler loginFailureHandler;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {try {//获取当前请求的url地址String url = request.getRequestURI();//如果当前请求不是登录请求,则需要进行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);}//登录请求不需要验证tokendoFilter(request, response, filterChain);}/***  校验token有效性* @param request* @throws AuthenticationException*/private void validateToken(HttpServletRequest request) throwsAuthenticationException {//从头部获取token信息String token = request.getHeader("token");//如果请求头部没有获取到token,则从请求的参数中进行获取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();//把字符串转成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);//创建身份验证对象UsernamePasswordAuthenticationToken authenticationToken = newUsernamePasswordAuthenticationToken(loginUser, null,loginUser.getAuthorities());//设置到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 {@Autowiredprivate 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 {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic R login(LoginUserReq userInf) {// 1 封装 authenticationToken 对象,密码校验等信息UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userInf.getLoginId(),userInf.getLoginPwd());// 2 开始调用进行校验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);//调用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);// 跳转到页面return R.ok().data("token",jwtStr).message("/main/index");}
}

2.6.3  代码截图

9e98872c695d479296c0086063bc1dfb.png

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 {@Autowiredprivate HttpServletRequest request;/***  获取图像验证码* @param response*/@RequestMapping("getVerifyImage")public void getVerifyImage(HttpServletResponse response) {RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);//定义图形验证码的长、宽、验证码位数、干扰线数量LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(120, 40,4,19);lineCaptcha.setGenerator(randomGenerator);lineCaptcha.createCode();//设置背景颜色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 前端效果

dfea2350b6a94fbebd579a4a64f25496.png

ee63507c28804e06bcd3bdc0419a8bb3.png

2.7.2 前端代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页</title><!-- 引入样式 --><link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><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: '请填写登录账号', trigger: 'blur' },{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }],loginPwd: [{ required: true, message: '请填写登录密码', trigger: 'blur' },{ min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }],imageCode: [{ required: true, message: '请填写图像验证码', 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: '登录成功,稍后进行跳转',type: 'success'});let url = resData.message + "?token=" + resData.data.tokenwindow.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  登录成功页面

8c3003ba0a5b453186dd03f379be088f.png

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" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><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-menudefault-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>导航一</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">导航二</span></el-menu-item><el-menu-item index="3" disabled><i class="el-icon-document"></i><span slot="title">导航三</span></el-menu-item><el-menu-item index="4"><i class="el-icon-setting"></i><span slot="title">导航四</span></el-menu-item></el-menu></el-col></el-row></el-aside><el-main>我是内容</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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/404472.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络工程3(子网通信,为什么要使用mac和ip)

文章目录 一. 子网如何通讯1. 子网内部通信2. 子网外部通信 二. 交换机和路由器的连接三. 为什么不只使用mac地址或ip地址进行网络通信1. 首先要明确的是&#xff0c;不管是只用mac或只用ip通信 四. 子网设备如何获得ip五. 不同网段的主机无法直接通信的原因 一. 子网如何通讯 …

音频矩阵主要功能及常规路数配置有哪些

音频矩阵&#xff0c;又称AUDIO矩阵或音频矩阵切换器&#xff0c;是一种用于管理和控制多个音频信号的设备。它具备多种功能&#xff0c;主要可以概括为以下几个方面&#xff1a; 一、主要功能 信号切换&#xff1a; AUDIO128128音频矩阵能够将多个音频源的信号输入到设备中&…

Python实现水果忍者(开源)

一、整体介绍&#xff1a; 1.1 前言&#xff1a; 游戏代码基于Python制作经典游戏案例-水果忍者做出一些改动&#xff0c;优化并增加了一些功能。作为自己Python阶段学习的结束作品&#xff0c;文章最后有源码链接。 1.2 Python主要知识&#xff1a; &#xff08;1&#xf…

怎么选开放式耳机好?精选五款物超所值机型推荐!

耳机已成为我们日常生活中的常见伙伴&#xff0c;无论是听音乐、玩游戏还是看剧&#xff0c;都离不开它。市场上耳机品牌和款式众多&#xff0c;找到一款真正适合自己的并不容易。尤其是长时间佩戴传统入耳式耳机可能会感到耳朵不舒服或闷热。开放式耳机因其非侵入式设计&#…

运维学习————Linux环境下Tomcat的部署

目录 一、环境准备 二、 启动测试 三、访问端口修改 四、部署web项目 1、材料准备 2、部署 2.1、上传war包到webapps目录下 2.2、修改项目配置 2.3、浏览器访问 引申 一、环境准备 tomcat安装包&#xff1a;apache-tomcat-9.0.52 JDK环境&#xff1a; 我使用的y…

MATLAB口罩检测系统

一、应用背景 作为数字图像处理和计算机视觉领域的一个重要组成部分&#xff0c;利用摄像机对图像进行采集&#xff0c;从图像中检测人脸并进行口罩穿戴的识别的有着非常重要的研究意义和应用价值。面对突如其来的新型肺炎疫情&#xff0c;人们生活秩序被严重打乱。跟普通流感…

LeetCode.80.删除有序数组中的重复项II

题目描述&#xff1a; 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O(1) 额外空间…

idea 修改背景图片教程

&#x1f30f;个人博客主页&#xff1a;意疏-CSDN博客 希望文章能够给到初学的你一些启发&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏支持一下笔者吧&#xff5e; 阅读指南&#xff1a; 开篇说明修改背景图片 开篇说明 给小白看得懂的修改图片教程&…

物流抓取机器人整体设计方案

一、功能简介 1、运行环境&#xff1a;巡线行驶&#xff08;7路数字循迹&#xff0c;麦克纳姆轮车底盘&#xff09; 2、目标识别&#xff1a;颜色识别&#xff08;Maix-II Dock 视觉模块&#xff09; 3、目标定位&#xff1a;视觉测距&#xff08;Maix-II Dock 视觉模块&#x…

【RPA学习天地:财务网银RPA方案】各银行网银(工行、招行、苏州银行等)流水和回单下载

RPA学习天地&#xff08;https://www.rpa-learning.com/&#xff09; 前言&#xff1a; 自 2021 年起&#xff0c;RPA 学习天地已成功为来自各个行业的数千名学员赋予了 RPA 技能。通过对众多学员在自动化需求方面的深入探究&#xff0c;我们发现财务的资金领域中与网银相关的…

【iOS】—— JSONModel

JSONModel源码 1. JSONModel介绍2. JSONModel的其他用法2.1 转换属性名称2.2 自定义错误 3. 源码分析3.1 - (id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err3.2 JSONModel持有的数据3.3 load3.4 JSONModel的init方法3.5 __inspectProperties方法3.6 JSONMode…

8月19日笔记

http隧道搭建(续) ABPTTS安装使用 一款基于 SSL 加密的 HTTP 端口转发工具&#xff0c;全程通信数据加密&#xff0c;比 reGerog 都要稳定。使用 python2 编写&#xff0c;但是该工具只支持 aspx 和 jsp 脚本的网站。 下载地址&#xff1a;https://github.com/nccgroup/ABPTT…

测试报告---自动化测试

一、测试用例 上文铺垫了基础知识。 https://blog.csdn.net/m0_74876421/article/details/141307905https://blog.csdn.net/m0_74876421/article/details/141307905 二、性能测试 1.准备功能&#xff1a; 浏览器驱动以及selenim包 引入依赖&#xff1a;在pom.xml文件中添加…

selenium底层原理详解

目录 1、selenium版本的演变 1.1、Selenium 1.x&#xff08;Selenium RC时代&#xff09; 1.2、Selenium 2.x&#xff08;WebDriver整合时代&#xff09; 1.3、Selenium 3.x 2、selenium原理说明 3、源码说明 3.1、启动webdriver服务建立连接 3.2、发送操作 1、seleni…

【性能优化】修复一个谷歌官方承认的内存泄漏问题

前言 通过下面这段代码&#xff0c;配合控制台可以直观看到谷歌官方承认的一个内存泄漏问题&#xff0c;https://issues.chromium.org/issues/41403456。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta …

前端css动画transform多个属性值写法

X轴平移400px transform: translateX(400px); X轴平移400px并缩小0.5倍 transform: translateX(400px) scale(0.5); X轴平移400px并旋转45度 transform: translateX(400px) rotate(45d…

备考2024年美国数学竞赛AMC10:吃透1250道真题和知识点(持续)

有什么含金量比较高的初中生数学竞赛吗&#xff1f;美国数学竞赛AMC10是个不错的选择。那么&#xff0c;如何备考AMC10美国数学竞赛呢&#xff1f;做真题&#xff0c;吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一。 通过做真题&#xff0c;可以帮助孩子找到真实竞赛…

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(二)---ROS2与UE5进行图像数据传输

前言 本系列教程旨在使用UE5配置一个具备激光雷达深度摄像机的仿真小车&#xff0c;并使用通过跨平台的方式进行ROS2和UE5仿真的通讯&#xff0c;达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础&#xff0c;Nav2相关的学习教程可以参考本人的其他博…

系规学习第13天

1、规划设计的主要目的不包括() A、设计满足业务需求的IT服务 B、设计SLA、测量方法和指标。 C、设计服务过程及其控制方 D、设计实施规划所需要的进度管理过程 [答案] D [解析]本题考察的是规划设计的目的&#xff0c;建议掌握。 (1)设计满足业务需求的IT服务。 (2)设…

Axios请求使用params参数导致后端获取数据嵌套

问题重述&#xff1a; 首先看前端的axios请求这里我使用params参数将data数据传给后端 let data JSON.stringify(this.posts);axios.post("/blog_war_exploded/insertPost", {params: {data: data}}).then((res) > {if (res.data "success") {alert(…