springsecurity 登录认证流程分析一(ajax)
目录
- 一、准备工作
- 1.1 导入依赖
- 二、认证
- 2.1 登录认证流程
- 2.3 自定义数据源分析
- 2.4 自定义数据源查询代码实现(可实现多数据源模式,DB2,mysql)
- 2.4.1 自定义数据源扫描mapper
- 2.4.2 自定义 UserDetailsService
- 2.4.3 自定义 UserDetails
- 2.4.4 创建用户sql
- 2.4.5 其他实体类(用户类)
- 2.4.6 通用返回类
- 2.5 配置类/工具类
- 2.5.1 webUtild 工具类
- 2.5.2 Redis 工具类配置
- 2.5.3 spring security 配置
- 2.5.4 web 配置静态资源放行等信息
- 2.5.5 异常类编写
- 2.5.6 过滤器(图形验证码过滤器)
- 2.5.7 jwt 过滤器
- 2.6 自定义登录接口
- 2.6.1 登录controller 接口
- 2.6.2 UserLoginService 用户自定义接口
- 2.6.3 代码截图
- 2.6.4 验证码controller
- 2.6.5 登录首页
- 2.7 前端页面
- 2.7.1 前端效果
- 2.7.2 前端代码
- 2.7.3 登录成功页面
- 2.7.4 htm 代码
一、准备工作
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> javascript <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 @Bean public DataSource mysqlDataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/lq"); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Primary @Bean public SqlSessionFactory mysqlSqlSessionFactory(@Autowired DataSource mysqlDataSource){ SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(mysqlDataSource); sessionFactory.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.XML")); try { // mapper xml 文件位置 sessionFactory.setMapperlocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mybatis/mapper/mysql/*.xml")); // sessionFactory.setMapperLocations(new ClassPathResource("/mybatis/mapper/mysql/*.xml")); return sessionFactory.getObject(); } catch (Exception e) { e.printStackTrace(); } return null; } }
2.4.2 自定义 UserDetailsService
package com.fashion.service; import com.fashion.domain.LoginSessionUserInf; import com.fashion.domain.mysql.TUserInf; import com.fashion.exception.CustomerAuthenticationException; import com.fashion.mapper.mysql.TUserInfMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import java.util.Arrays; import java.util.List; /** * @Author: LQ * @Date 2024/8/13 21:12 * @Description: */ @Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private TUserInfMapper userInfMapper; @Override public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException { // 根据用户名获取用户信息 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; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (grantedAuthorities != null) { return grantedAuthorities; } grantedAuthorities = perList.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return grantedAuthorities编程客栈; } @Override public String getPassword() { return userInf.getLoginPwd(); } @Override public String getUsername() { return userInf.getLoginId(); } //判断账号是否未过期 @Override public boolean isAccountNonExpired() { return "1".equals(userInf.getStatus()); } //判断账号是否没有锁定 @Override public boolean isAccountNonLocked() { return true; } //判断账号是否没有超时 @Override public boolean isCredentialsNonExpired() { return true; } //判断账号是否可用 @Override public boolean isEnabled() { return true; } }
2.4.4 创建用户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 { @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<&编程gt;(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.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.WebSecurityConfigurerandroidAdapter; 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.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 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } /** * 配置默认首页地址 * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); } // @Override // public void addCorsMappings(CorsRegistry registry) { // registry.addMapping("/**") // .allowedOrigins("*") // .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // .allowedHeaders("*") // .allowCredentials(true); // } }
2.5.5 异常类编写
/** * @Author: LQ * @Date 2024/8/17 20:29 * @Description: */ public class CustomerAccessException extends AccessDeniedException { public CustomerAccessException(String msg) { super(msg); } } /** * @Author: LQ * @Date 2024/8/17 15:35 * @Description: 无权限资源时异常 */ public class CustomerAuthenticationException extends AuthenticationException { public CustomerAuthenticationException(String msg) { super(msg); } }
2.5.6 过滤器(图形验证码过滤器)
package com.fashion.filter; import com.fashion.constants.ComConstants; import com.fashion.domain.R; import com.fashion.handler.AnonymousAuthenticationHandler; import com.fashion.utils.WebUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Author: LQ * @Date 2024/8/17 19:29 * @Description: 图像验证码过滤器 */ @Component @Slf4j public class ImgVerifyFilter extends OncePerRequestFilter { @Autowired private HttpServletRequest request; @Autowired private AnonymousAuthenticationHandler anonymousAuthenticationHandler; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String reqUrl = httpServletRequest.getRequestURI(); log.info("请求url:{}",reqUrl); if (ComConstants.LOGIN_URL.equals(reqUrl)) { // 开始校验图形验证码 Object imgCode = request.getParameter("imageCode"); Object sessCode = request.getSession().getAttribute(ComConstants.SESSION_IMAGE); // 判断是否和库里面相等 log.info("传过来的验证码为:{},session中的为:{}",imgCode,sessCode); if (!sessCode.equals(imgCode)) { //throw new CustomerAuthenticationException("图像验证码错误"); WebUtils.writeResp(httpServletResponse, R.error().code(400).message("图像验证码失败!")); return; } } filterChain.doFilter(httpServletRequest,httpServletResponse); } }
2.5.7 jwt 过滤器
作用:因为禁用了session所以需要将 SecurityContextHolder.getContext() 中
package com.fashion.filter; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.fashion.constants.ComConstants; import com.fashion.constants.RedisPreConst; import com.fashion.domain.JwtToken; import com.fashion.domain.LoginSessionUserInf; import com.fashion.exception.CustomerAuthenticationException; import com.fashion.handler.LoginFailureHandler; import com.fashion.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Author: LQ * @Date 2024/8/17 22:12 * @Description: jwt 认证 */ @Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { //获取当前请求的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.6 自定义登录接口
2.6.1 登录controller 接口
package com.fashion.controller; import com.fashion.domain.R; import com.fashion.domain.req.LoginUserReq; import com.fashion.service.UserLoginService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @Author: LQ * @Date 2024/8/17 16:05 * @Description: 用户登录接口 */ @RestController @RequestMapping("user/") public class UserLoginController { @Autowired private UserLoginService userLoginService; /** * 用户登录 * @param req * @return */ @RequestMapping("login") public R userLogin(LoginUserReq req) { return userLoginService.login(req); } }
2.6.2 UserLoginService 用户自定义接口
package com.fashion.service; import com.fashion.domain.R; import com.fashion.domain.req.LoginUserReq; /** * @Author: LQ * @Date 2024/8/17 16:07 * @Description: 用户自定义登录重写 ProviderManager的方法进行认证 如果认证通过生成jw */ public interface UserLoginService { /** * 登录 * @param userInf * @return */ R login(LoginUserReq userInf); } @Service @Slf4j public class UserLoginServiceImpl implements UserLoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public R login(LoginUserReq userInf) { // 1 封装 authenticationToken 对象,密码校验等信息 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userInf.getLoginId(),userInf.getLoginPwd()); // 2 开始调用进行校验 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 { @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.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" rel="external nofollow" rel="external nofollow" > <style type="text/css"> #app{width: 600px;margin: 28px auto 10px } img{cursor: pointer;} </style> </head> <body> <div id="app"> <el-container> <el-header> <h2>欢迎进入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')">登录</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.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" rel="external nofollow" rel="external nofollow" > <style type="text/css"> </style> </head> <body> <div id="app"> <el-container> <el-header> <h2 >欢迎进入springsecurity 配置主页</h2> </el-header> <el-container> <el-aside width="400px"> <el-row class="tac"> <el-col :span="12"> <h5>菜单</h5> <el-menu default-active="2" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose"> <el-submenu index="1"> <template slot="title"> <i class="el-icon-location"></i> <span>导航一</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>
到此这篇关于springsecurity 登录认证一(AJAX)的文章就介绍到这了,更多相关springsecurity 登录认证内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论