一、准备工作
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.2 自定义数据源分析
①自定义登录接口 调用ProviderManager的方法进行认证 如果认证通过生成jwt 把用户信息存入redis中;
②自定义UserDetailsService 在这个实现类中去查询数据库;
2.2 自定义数据源查询代码实现(可实现多数据源模式,db2,mysql)
2.2.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
@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.2.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 {
// 根据用户名获取用户信息
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.2.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;
}
@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.2.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.2.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.2.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.3 配置类/工具类
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.3.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.3.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);
// 此设置默认为true,就是在反序列化遇到未知属性时抛异常,这里设置为false,目的为忽略部分序列化对象存入缓存时误存的其他方法的返回值
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
2.3.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 {
@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");
/**
* 登录时需要调用AuthenticationManager.authenticate执行一次校验
*
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// 入口配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭crsf
http.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);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3.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
*/
@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.3.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.3.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.3.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 {
//获取当前请求的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);
}
//登录请求不需要验证token
doFilter(request, response, filterChain);
}
/**
* 校验token有效性
* @param request
* @throws AuthenticationException
*/
private void validateToken(HttpServletRequest request) throws
AuthenticationException {
//从头部获取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 = new
UsernamePasswordAuthenticationToken(loginUser, null,
loginUser.getAuthorities());
//设置到Spring Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
2.4 自定义登录接口
2.4.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.4.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 开始调用进行校验
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.4.3 代码截图
2.4.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);
//定义图形验证码的长、宽、验证码位数、干扰线数量
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.4.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.5 前端页面
2.5.1 前端效果
2.5.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.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.5.3 登录成功页面
2.5.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-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>导航一</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>