springsecurity 登录认证一(ajax)

一、准备工作

        1.1 导入依赖

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

ccbeefb65915450ab5d5c8120181bcc2.png

dd5e902bdd0349afa9b846999d2d214b.png

  <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
       <!-- <version>2.7.18</version>-->
    </parent>




 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- thymeleaf 相关依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>

        <!-- mybatis坐标 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
           <!-- <version>8.0.28</version>-->
        </dependency>
        <!--validation依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!--redis坐标-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--springdoc-openapi-->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

    </dependencies>


 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

二、认证

        2.1 登录认证流程

54c0811aeadb4ec7a59ae22a26dff4e2.png

接口解释

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

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

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

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

2.2  自定义数据源分析

11932b7c43d74f7c96acd4e781385ee2.png

①自定义登录接口 调用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  代码截图

9e98872c695d479296c0086063bc1dfb.png

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 前端效果

dfea2350b6a94fbebd579a4a64f25496.png

ee63507c28804e06bcd3bdc0419a8bb3.png

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  登录成功页面

8c3003ba0a5b453186dd03f379be088f.png

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>

 

 

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值