一、准备工作
1.1 导入依赖
因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 相关依赖 --><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接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息;
AuthenticationManager接口:定义了认证Authentication的方法;
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的 方法;
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装 成UserDetails对象返回。然后将这些信息封装到Authentication对象中;
2.3 自定义数据源分析
①自定义登录接口 调用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 代码截图
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 前端效果
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 登录成功页面
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>