springboot2.x+security+vue2.x后台管理框架---security配置(三)

security配置

一、security介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

二、security核心配置

引入依赖

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

1、security(springboot2.7.3)安全配置

package com.longzy.config;

import com.longzy.security.*;
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.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;


/**
 * @Desc: security安全配置(springboot2.7.3)
 * @Packge: com.longzy.config
 * @Author: longzy
 * @Date: 2022/9/6 11:25
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private CustomLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private CustomUserDetailService userDetailService;

    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Autowired
    private WebSecurityConfig webSecurityConfig;

    @Bean
    public CustomCaptchaFilter customCaptchaFilter(){
        return new CustomCaptchaFilter();
    }

    //获取AuthenticationManager(认证管理器),登录时认证使用
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() {
        return new CustomAuthenticationFilter();
    }

    /**
     * 配置加密方式
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置过滤
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.csrf()
                .disable().cors().and() //关闭cors和csrf
                // 登录配置
                .formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)

                // 登出配置
                .and()
                    .logout()
                    .logoutSuccessHandler(logoutSuccessHandler)

                // 禁用session
                .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                // 配置拦截规则
                .and()
                    .authorizeRequests()
                    .antMatchers(webSecurityConfig.getWhiteListUrls()).permitAll()
                    .anyRequest().authenticated()

                // 异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(customAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler)

                // 自定义过滤器配置
                .and()
                    .userDetailsService(userDetailService)
                    .authenticationProvider(customAuthenticationProvider)
                    .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(customCaptchaFilter(), UsernamePasswordAuthenticationFilter.class)
                    .build();
    }

    /**
     * 跨域处理(cors)
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
//        // * 表示对所有的地址都可以访问
//        corsConfiguration.addAllowedOrigin("*");
//        //  跨域的请求头
//        corsConfiguration.addAllowedHeader("*");
//        //  跨域的请求方法
//        corsConfiguration.addAllowedMethod("*");
//        //加上了这一句,大致意思是可以携带 cookie
//        //最终的结果是可以 在跨域请求的时候获取同一个 session
        corsConfiguration.addExposedHeader("Authorization");
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*") );
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        corsConfiguration.setAllowCredentials(true);


        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //配置 可以访问的地址
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
}

2、放行资源配置类

yml配置白名单

spring:
  security:
    #放行资源(未登录)
    white-list-urls:
      - /captcha

未登录放行资源

package com.longzy.config;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * @Desc: 放行资源配置类
 * @Packge: com.longzy.config
 * @Author: longzy
 * @Date: 2022/9/21 20:00
 */
@Component
@ConfigurationProperties(value = "spring.security")
public class WebSecurityConfig {

    private static final List<String> DEFAULT_WHITE_LIST_URLS =
            Arrays.asList("/login", "/logout", "/favicon.ico", "/doc.html",
                    "/*/api-docs", "/swagger-resources/**", "/swagger-ui.html", "/swagger-ui/*", "/webjars/**");

    private Set<String> whiteListUrls;

    public WebSecurityConfig(Set<String> whiteListUrls) {
        this.whiteListUrls = whiteListUrls;
    }

    public String[] getWhiteListUrls() {
        return this.whiteListUrls.toArray(new String[this.whiteListUrls.size()]);
    }

    public void setWhiteListUrls(Set<String> whiteListUrls){
        if (this.whiteListUrls != whiteListUrls){
            this.whiteListUrls.clear();
            this.whiteListUrls.addAll(whiteListUrls);
            this.whiteListUrls.addAll(DEFAULT_WHITE_LIST_URLS);
        }
    }
}

3、自定义登录成功处理器

(1)、jwt生成工具类

引入依赖

<!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

yml配置 设置token 8小时过期

# jwt 配置
token:
  jwt:
    header: Authorization
    expire: 28800 #过期时间,单位秒
    secret: longzy1285asd456gdd15lhsd4lo6x7e #32位

JwtUtils.java

package com.longzy.common.utils;

import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "token.jwt")
public class JwtUtils {

	private long expire;
	private String secret;
	private String header;

	// 生成jwt
	public String generateToken(String username) {

		Date nowDate = new Date();
		Date expireDate = new Date(nowDate.getTime() + 1000 * expire);

		return Jwts.builder()
				.setHeaderParam("typ", "JWT")
				.setSubject(username)
				.setIssuedAt(nowDate)
				.setExpiration(expireDate)// 过期时间
				.signWith(SignatureAlgorithm.HS512, secret)
				.compact();
	}

	// 解析jwt
	public Claims getClaimByToken(String jwt) {
		try {
			return Jwts.parser()
					.setSigningKey(secret)
					.parseClaimsJws(jwt)
					.getBody();
		} catch (Exception e) {
			return null;
		}
	}

	// jwt是否过期
	public boolean isTokenExpired(Claims claims) {
		return claims.getExpiration().before(new Date());
	}

}

(2)、登录成功处理器
package com.longzy.security;

import cn.hutool.json.JSONUtil;
import com.longzy.component.user.entity.LoginUser;
import com.longzy.common.response.CommonReturnType;
import com.longzy.common.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Desc: 登录成功操作类
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/5 22:30
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        // 生成jwt,并放置到请求头中并将用户信息返回
        String jwt = jwtUtils.generateToken(authentication.getName());
        response.setHeader(jwtUtils.getHeader(), jwt);

        // 返回用户信息
        LoginUser user = (LoginUser) authentication.getPrincipal();
        user.setPassword(null); //密码不可传入前端

        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(JSONUtil.toJsonStr(CommonReturnType.success("登录成功", user)).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

4、自定义失败处理器

package com.longzy.security;

import cn.hutool.json.JSONUtil;
import com.longzy.common.response.CommonReturnType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Desc: 登录失败处理类
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/5 22:22
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        CommonReturnType result = CommonReturnType.error(exception.getMessage());

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

5、自定义退出处理器

package com.longzy.security;

import cn.hutool.json.JSONUtil;
import com.longzy.common.response.CommonReturnType;
import com.longzy.common.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Desc: 自定义登出(jwt清除)类
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/5 23:14
 */
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }

        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        response.setHeader(jwtUtils.getHeader(), "");

        outputStream.write(JSONUtil.toJsonStr(CommonReturnType.success("退出成功")).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

6、自定义用户信息封装

package com.longzy.security;

import cn.hutool.core.convert.Convert;
import com.longzy.component.user.entity.LoginUser;
import com.longzy.component.user.entity.SysUserPo;
import com.longzy.component.user.service.read.SysUserReadService;
import com.longzy.component.user.vo.SysUserVo;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
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.Service;

/**
 * @Desc: 自定义用户信息封装
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/6 18:49
 */
@Service
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private SysUserReadService sysUserReadService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserVo userVo = sysUserReadService.getUserByLoginId(username);
        if (ObjectUtils.isEmpty(userVo)){
            throw new UsernameNotFoundException("账号不存在.");
        }
        LoginUser user = new LoginUser();
        BeanUtils.copyProperties(userVo, user);
        return user;
    }
}

查询用户接口

package com.longzy.component.user.service.read;

import com.github.pagehelper.PageInfo;
import com.longzy.component.user.vo.SysUserVo;
import com.longzy.common.page.PageParam;

/**
 * @Desc:
 * @Packge: com.longzy.component.user.service.read
 * @Author: longzy
 * @Date: 2022/9/5 11:58
 */
public interface SysUserReadService {
    SysUserVo getUserByLoginId(String loginId);
}

接口实现类

package com.longzy.component.user.service.read.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.longzy.component.user.mapper.read.SysUserReadMapper;
import com.longzy.component.user.service.read.SysUserReadService;
import com.longzy.component.user.vo.SysUserVo;
import com.longzy.error.BusinessException;
import com.longzy.common.page.PageParam;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Desc:
 * @Packge: com.longzy.component.user.service.read.impl
 * @Author: longzy
 * @Date: 2022/9/5 11:58
 */
@Service
public class SysUserReadServiceImpl implements SysUserReadService {

    @Resource
    private SysUserReadMapper sysUserReadMapper;

    @Override
    public SysUserVo getUserByLoginId(String loginId) {
        if (StringUtils.isEmpty(loginId)){
            throw new BusinessException("请输入用户名.");
        }
        SysUserVo userVo = sysUserReadMapper.queryUserByLoginId(loginId, "1", "0");
        if (ObjectUtils.isEmpty(userVo)){
            throw new BusinessException("账号不存在.");
        }
        return userVo;
    }

}

mapper

package com.longzy.component.user.mapper.read;

import com.longzy.component.user.vo.SysUserVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
*
* @Desc:
* @Packge: com.longzy.component.user.mapper.read
* @Author: longzy
* @Date: 2022/9/5 12:00
*/
public interface SysUserReadMapper {
    /**
     * 根据loginId查询用户
     * @param loginId 登录账号
     * @param effective 有效状态
     * @param destory 销毁状态
     * @return
     */
    SysUserVo queryUserByLoginId(@Param("loginId") String loginId, @Param("effective") String effective, @Param("destory") String destory);
}

sql

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--suppress ALL -->
<mapper namespace="com.longzy.component.user.mapper.read.SysUserReadMapper">
    
    <sql id="Base_Column_List">
        userid,
        loginid,
        name,
        sex,
        password,
        idcardno,
        mobile,
        email,
        createuser,
        createtime,
        effective,
        destory,
        field01,
        field02,
        field03,
        field04,
        filed05,
        field06,
        field07,
        field08,
        field09,
        field10
    </sql>

    <select id="queryUserByLoginId" resultType="com.longzy.component.user.vo.SysUserVo">
        select
        <include refid="Base_Column_List"/>
        from sys_user
        where effective = #{effective,jdbcType=VARCHAR}
         and destory = #{destory,jdbcType=VARCHAR}
         and loginid = #{loginId,jdbcType=VARCHAR}
    </select>

</mapper>

7、自定义认证失败处理器

package com.longzy.security;

import cn.hutool.json.JSONUtil;
import com.longzy.common.response.CommonReturnType;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Desc: 认证失败处理器
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/5 23:18
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        String message = "";
        if (authException instanceof InsufficientAuthenticationException){ // token过期之类的
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            message = "未登录,请重新登录认证.";
        }else {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            message = "权限不足,请联系系统管理员.";
        }
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(JSONUtil.toJsonStr(CommonReturnType.error(message)).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

8、自定义异常处理器

package com.longzy.security;

import cn.hutool.json.JSONUtil;
import com.longzy.common.response.CommonReturnType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Desc: 自定义异常处理器
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/6 18:45
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

        ServletOutputStream outputStream = response.getOutputStream();

        outputStream.write(JSONUtil.toJsonStr(CommonReturnType.error(e.getMessage())).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();

    }
}

9、验证码生成

(1)、引入依赖
<!--验证码-->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
(2)、验证码配置文件
package com.longzy.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * @Desc: 验证码
 * @Packge: com.longzy.config
 * @Author: longzy
 * @Date: 2022/9/5 21:12
 */
@Configuration
public class CaptchaConfig {

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        //验证码生成器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        //配置
        Properties properties = new Properties();
        //是否有边框
        properties.setProperty("kaptcha.border", "yes");
        //设置边框颜色
        properties.setProperty("kaptcha.border.color", "105,179,90");
        //边框粗细度,默认为1
        // properties.setProperty("kaptcha.border.thickness","1");
        //验证码
        properties.setProperty("kaptcha.session.key", "code");
        //验证码文本字符颜色 默认为黑色
        properties.setProperty("kaptcha.textproducer.font.color", "blue");
        //设置字体样式
//        properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
        //字体大小,默认40
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        //验证码文本字符内容范围 默认为abced2345678gfynmnpwx
        // properties.setProperty("kaptcha.textproducer.char.string", "");
        //字符长度,默认为5
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字符间距 默认为2
        properties.setProperty("kaptcha.textproducer.char.space", "4");
        //验证码图片宽度 默认为200
        properties.setProperty("kaptcha.image.width", "100");
        //验证码图片高度 默认为40
        properties.setProperty("kaptcha.image.height", "40");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }


}

(3)、生成验证码
package com.longzy.component.login;

import cn.hutool.core.map.MapUtil;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.longzy.component.user.entity.LoginUser;
import com.longzy.component.user.service.read.SysUserReadService;
import com.longzy.component.user.vo.SysUserVo;
import com.longzy.constant.Const;
import com.longzy.error.BusinessException;
import com.longzy.common.response.CommonReturnType;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Principal;
import java.util.UUID;

/**
 * @Desc: 登录相关
 * @Packge: com.longzy.component.login
 * @Author: longzy
 * @Date: 2022/9/5 21:30
 */
@Api(tags = "登录模块")
@RestController
public class LoginController {

    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @Autowired
    private SysUserReadService sysUserReadService;

    @ApiOperation(value = "生成验证码")
    @GetMapping(value = "captcha")
    public CommonReturnType captcha(HttpServletRequest request, HttpServletResponse response){
        String token = UUID.randomUUID().toString();
        //获取验证码文本内容
        String text = defaultKaptcha.createText();
        //将验证码放到session中
        request.getSession().setAttribute(Const.CAPTCHA_KEY,text);
        //根据文本内容创建图形验证码
        BufferedImage image = defaultKaptcha.createImage(text);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        String base64Img = null;
        try {
            ImageIO.write(image, "jpg", outputStream);
            String str = "data:image/jpeg;base64,";
            base64Img = str + Base64.encodeBase64String(outputStream.toByteArray());
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return CommonReturnType.success(null,
                MapUtil.builder().put("captchaImg", base64Img).put("token", token).build());
    }
}

(4)、验证码过滤器
package com.longzy.security;

import com.longzy.constant.Const;
import com.longzy.error.CaptchaException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
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.BufferedReader;
import java.io.IOException;

/**
 * @Desc: 验证码过滤器
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/9 8:56
 */
public class CustomCaptchaFilter extends OncePerRequestFilter {

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 验证码校验
        String url = request.getRequestURI();
        if ((request.getContextPath() + "/login").equals(url) && request.getMethod().equals("POST")){
            try {
                validate(request);
            } catch (AuthenticationException e) {
                e.printStackTrace();
                loginFailureHandler.onAuthenticationFailure(request, response, e);
            }
        }

        filterChain.doFilter(request, response);
    }
    //校验验证码逻辑
    private void validate(HttpServletRequest request) {
        String code = request.getParameter("code");
        String key = request.getParameter("token");

        if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
            throw new CaptchaException("验证码错误,请重新输入.");
        }

        String sessionCode = (String)request.getSession().getAttribute(Const.CAPTCHA_KEY);
        if (!code.equals(sessionCode)) {
            throw new CaptchaException("验证码错误,请重新输入.");
        }
        // 验证码一次性使用
        request.getSession().removeAttribute(Const.CAPTCHA_KEY);
    }
}

(5)、验证码访问

在这里插入图片描述

10、token过期处理

package com.longzy.security;

import cn.hutool.core.util.StrUtil;
import com.longzy.component.user.service.read.SysUserReadService;
import com.longzy.component.user.vo.SysUserVo;
import com.longzy.common.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
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;

/**
 * @Desc: 自定义根据token自动登录
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/5 23:21
 */
public class CustomAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private SysUserReadService sysUserReadService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        // token校验
        // 获取jwt
        String jwt = request.getHeader(jwtUtils.getHeader());
        if (StrUtil.isBlankOrUndefined(jwt)){
            chain.doFilter(request, response);
            return;
        }
        // 解析token
        Claims claims = jwtUtils.getClaimByToken(jwt);
        if (claims == null){
            throw new JwtException("token 异常");
        }
        // 判断token是否过期
        if (jwtUtils.isTokenExpired(claims)){
            throw new JwtException("token 已过期");
        }
        // 获取用户名
        String username = claims.getSubject();
        // 获取用户信息和权限
        SysUserVo userVo = sysUserReadService.getUserByLoginId(username);
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(username, null, null);

        SecurityContextHolder.getContext().setAuthentication(token);

        chain.doFilter(request, response);
    }
}

11、自定义登录处理器

(1)、RSA加密解密工具类
package com.longzy.common.utils;

import com.longzy.error.BusinessException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;


/**
 * @Desc: RSA加密解密工具类
 * @Packge: com.longzy.common.utils
 * @Author: longzy
 * @Date: 2022/9/13 11:43
 */
public class RSAUtils {

    private static final Logger log = LoggerFactory.getLogger(RSAUtils.class);

    private static final String ALGORITHM = "RSA";
    // 秘钥
    public static final String PRIVATE_KEY_PATH = "privatekey.pem";
    public static final String PUBLIC_KEY_PATH = "publickey.pem";
    public static volatile PublicKey PUBLICKEY;
    public static volatile PrivateKey PRIVATEKEY;

    // 加密算法
    private static final String CIPHER_DE = "RSA";
    // 解密算法
    private static final String CIPHER_EN = "RSA";
    // 密钥长度
    private static final Integer KEY_LENGTH = 2048;
    // RSA最大加密明文大小
    private static final int MAX_ENCRYPT_BLOCK = 117;
    //RSA最大解密密文大小
    private static final int MAX_DECRYPT_BLOCK = 256;

    static {
        try {
            getPrivateKey();
            getPublicKey();
        } catch (Exception e) {
            if (log.isErrorEnabled()){
                log.error("加载密钥失败.");
            }
        }
    }

    /**
     * 生成秘钥对,公钥和私钥
     * @return
     * @throws BusinessException
     */
    public static KeyPair genKeyPair() throws BusinessException {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
            keyPairGenerator.initialize(KEY_LENGTH); // 秘钥字节数
            return keyPairGenerator.generateKeyPair();
        }catch (Exception e){
            throw new BusinessException("生成秘钥对出错,请检查秘钥文件是否正确!");
        }
    }

    /**
     * 加密
     * @param data 明文
     * @return
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data) throws Exception {
        // 加密数据,分段加密
        Cipher cipher = Cipher.getInstance(CIPHER_EN);
        cipher.init(Cipher.ENCRYPT_MODE, PUBLICKEY);
        int inputLength = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offset = 0;
        int i = 0;
        while (inputLength - offset > 0) {
            byte[] cache;
            if (inputLength - offset > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(data, offset, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offset, inputLength - offset);
            }
            out.write(cache, 0, cache.length);
            i++;
            offset = i * MAX_ENCRYPT_BLOCK;
        }
        byte[] encryptedData = out.toByteArray();
        out.close();
        return encryptedData;
    }


    /**
     * 解密
     * @param data 明文
     * @return
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data) throws Exception {
        // 解密数据,分段解密
        Cipher cipher = Cipher.getInstance(CIPHER_DE);
        cipher.init(Cipher.DECRYPT_MODE, PRIVATEKEY);
        int inputLength = data.length;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offset = 0;
        int i = 0;
        while (inputLength - offset > 0) {
            byte[] cache;
            if (inputLength - offset > MAX_DECRYPT_BLOCK) {
                cache = cipher.doFinal(data, offset, MAX_DECRYPT_BLOCK);
            } else {
                cache = cipher.doFinal(data, offset, inputLength - offset);
            }
            out.write(cache);
            i++;
            offset = i * MAX_DECRYPT_BLOCK;
        }
        byte[] decryptedData = out.toByteArray();
        out.close();
        return decryptedData;
    }


    /**
     * 读取私/公钥文件
     * @param path 文件路径
     * @return
     */
    public static InputStream getResourceAsStream(String path){
        InputStream in = null;
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        if (contextClassLoader != null){
            in = contextClassLoader.getResourceAsStream(path);
        }
        if (null == in){
            throw new BusinessException("私钥文件[" + path + "]不存在.");
        }
        return in;
    }

    /**
     *  获取公钥
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PublicKey getPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        // 得到公钥
        byte[] keyBytes = IOUtils.toByteArray(getResourceAsStream(PUBLIC_KEY_PATH));
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(keyBytes));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        // 赋值给公钥
        PUBLICKEY = keyFactory.generatePublic(x509EncodedKeySpec);
        return PUBLICKEY;
    }

    /**
     * 获取私钥
     * @return
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        // 得到私钥
        byte[] keyBytes = IOUtils.toByteArray(getResourceAsStream(PRIVATE_KEY_PATH));
        PKCS8EncodedKeySpec pKCS8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(keyBytes));
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        PRIVATEKEY = keyFactory.generatePrivate(pKCS8EncodedKeySpec);
        return PRIVATEKEY;
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = genKeyPair();
        System.out.println("public_key--->" + keyPair.getPublic());
        System.out.println("private_key--->" + keyPair.getPrivate());
        String plainText = "111111";
        String en = Base64.encodeBase64String(encrypt(plainText.getBytes(StandardCharsets.UTF_8)));
        System.out.println("加密--->" + en);
        String c = "mDpMv1QB3OaoKKPBJ+XcjzbGtCovkpIjtWj/lW7gbCgMsPyC1w38ZqG8lxBO02b6R9f40QiuqWWfQg/MzuEcZzgewm+g8VH9xrB2D86oi/JK4PRQTZ86H3m5UhkF88fuuM1q8t1q7L/cY01aMxU5uX+WBn1q9Ur2robncZWr5WpbrvmKvJJw4KcvOCuWtqcfh6D/AggtFPLEM3w6xe8vEeqb1/9zVtyhIQd8SyVBlRacpWR2SG0MwlbBf59uMTgYn55GFv+FGFa0Wbeoozn4M9SYLQ1TUsdXUVI7iq1Ai1/QU95WhqTFtSR9OhViAFLorv0VDBtoG9pMYGQRXAVl5g==";
        System.out.println("解密--->" + new String(decrypt(Base64.decodeBase64(c))));
    }

}


(2)、自定义登录
package com.longzy.security;

import com.longzy.component.user.entity.LoginUser;
import com.longzy.common.utils.RSAUtils;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * @Desc: 自定义登录
 * @Packge: com.longzy.security
 * @Author: longzy
 * @Date: 2022/9/8 23:25
 */
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailService customUserDetailService;

//    @Autowired
//    private PasswordEncoder passwordEncoder;

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @SneakyThrows
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 用户名和密码是通过RSA加密的
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        // 解密
        username = new String(RSAUtils.decrypt(Base64.decodeBase64(username.getBytes(StandardCharsets.UTF_8))));
        password = new String(RSAUtils.decrypt(Base64.decodeBase64(password.getBytes(StandardCharsets.UTF_8))));
        // 获取用户信息
        LoginUser userDetails = (LoginUser) customUserDetailService.loadUserByUsername(username);
        if (ObjectUtils.isEmpty(userDetails)){
            throw new UsernameNotFoundException("账号不存在.");
        }
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("密码不正确.");
        }
        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

三、自定义vue登录页面

1、vue

<template>
    <div class="login_container">
      <div class="login_box">
        <!-- 头像区域 -->
        <div class="avatar_box">
          <img src="@/assets/logo.png" alt="">
        </div>
        <!-- 登录表单区域 -->
        <el-form ref="loginForm" :rules="loginRules" :model="loginForm" label-width="0px" class="login_form">
          <!-- 用户名 -->
          <el-form-item prop="username">
            <el-input v-model="loginForm.username" prefix-icon="el-icon-user" placeholder="请输入用户名"/>
          </el-form-item>
          <!-- 密码 -->
          <el-form-item prop="password">
            <el-input v-model="loginForm.password" prefix-icon="el-icon-lock" type="password" placeholder="请输入密码"/>
          </el-form-item>
          <!-- 验证码 -->
          <el-form-item prop="code" style=" text-align: left">
            <el-input 
              v-model="loginForm.code" 
              prefix-icon="el-icon-key" 
              placeholder="请输入验证码" 
              style="width: 60%; float: left; margin-right: 10px;"/>
            <div>
              <el-image 
                :src="captchaImg"
                class="captchaImg" 
                style="width: 24%; float: left; margin-right: 10px; " 
                title="看不清换一张" 
                @click="fnCaptchaImg">
              </el-image>
            </div>
            <div>
              <label style="width: 44px; height: 20px; line-height: 20px;float: left; color: blue;" @click="fnCaptchaImg">看不清换一张</label>
            </div>
          </el-form-item>
          <el-form-item>
            <label style="float: left; color: red; font-size: 10px" >{{failMsg}}</label>
            <el-button type="text" style="float: right; margin-bottom: 0px;" >修改密码</el-button>
            <el-button type="primary" style="width: 100%; margin-left: 0px;" @click="fnSubmit('loginForm')" >登录</el-button>
          </el-form-item>
        </el-form>
      </div>
        
    </div>
</template>

<script>
  import $api from './api/index'
  import qs from 'qs'
  import encrypt from '@/util/JSEncrypt'

  export default {
    name: 'login',
    data() {
      return {
        loginForm: {
          username: 'admin',
          password: '111111',
          code: '',
          token: '',
        },
        captchaImg: '',
        failMsg: '',
        // 校验规则
        loginRules: {
          username: [
            { required: true, message: '请输入用户名', trigger: 'change'}
          ],
          password: [
            { required: true, message: '请输入密码', trigger: 'change' }
          ],
          code: [
            { required: true, message: '请输入验证码', trigger: 'change' }
          ]
        }
      }
    },
    mounted() {
      this.fnCaptchaImg()
    },
    methods: {
      // 登录
      fnSubmit(formName){
        this.failMsg = ''
        this.$refs[formName].validate((valid) => {
          if (valid){
            this.loginForm.username = encrypt(this.loginForm.username)
            this.loginForm.password = encrypt(this.loginForm.password)
            this.$request.post('/login?' + qs.stringify(this.loginForm)).then(res =>{
              if (res.data.code != 200){
                this.fnCaptchaImg()
                return;
              }
              const jwt = res.headers['authorization']
              this.$store.commit('SET_TOKEN', jwt);
              this.$store.commit('SET_USERINFO', res.data.data)
              this.$router.push("/")
            })
          }
        })
      },

      // 获取验证码
      fnCaptchaImg(){
        $api.getCaptchaImg().then(res => {
          if (res.data.code = 200){
            this.captchaImg = res.data.data.captchaImg
            this.loginForm.token = res.data.data.token
          }
        })
      },
    }
  }

</script>

<style lang="less" scoped>
  .login_container {
    width: 100%;
    height: 100%;
    background-image: url('@/assets/banner1.png');
    position:fixed;
    background-size:100% 100%;
    background-repeat: no-repeat;
  }

  .login_box {
    width: 450px;
    height: 380px;
    background-color: white;
    border-radius: 3px;
    /*容器内居中*/
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);

    .avatar_box {
      height: 130px;
      width: 130px;
      border: 1px solid #eee;
      border-radius: 50%;
      padding: 10px;
      /* 边框阴影 */
      box-shadow: 0 0 10px #ddd;
      position: absolute;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: #fff;

      img {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        background-color: #eee;
      }
    }

    .login_form {
      position: absolute;
      bottom: 0;
      width: 100%;
      padding: 0 20px;
      box-sizing: border-box;
    }
  }
</style>

2、密码加密js

import JSEncrypt  from 'jsencrypt';

// 加密
const encryptPwd = function(data) {
    // 加密公钥
    let publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArW8njkAlKTCQa6P71MHZj963AK2AWeHBo3BiKMz+PY70hBCoiqLd4izjBMzGKGm4ztjekc0VuGuyzZMHa7+JO4Zy8PHIrMfKHUB+jAYvKEjEJH0CRvfjzFf3zHPBacNPXwuCCrwzrBc6/iWw7WI4BT6eBea7N91Bm1RFemjQbTSQk7KZrZqn7kxM+WJSKSETgmmGp6f4vlPUOZpHK53d68sidlvTOAzWW5LXCNG3MDpfquE9Xk18/DsbkJOsCy+HBloCAmxd/GZXqmJsIfJ0jNFn7JaoE5gSQ1Jys/KFCSyVtD4IZWQ7KuFEew0Jzso71dHoxcSGSoagcQgN2lbo+wIDAQAB";
     // 新建JSEncrypt对象
    let encryptor = new JSEncrypt();
    // 设置公钥
    encryptor.setPublicKey(publicKey);
    var jm_data = encryptor.encrypt(data);
    // 加密数据
    return jm_data;
}

export default encryptPwd

3、 store数据配置

import Vue from 'vue'
import Vuex from 'vuex'
import loginJs from '@/views/core/login/api/index'
import dict from '@/store/modules/dict'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isCollapse: false,
    token: '',
    userInfo: {},
  },
  getters: {
  },
  mutations: {
    // token
    SET_TOKEN: (state, token) => {
			state.token = token
			localStorage.setItem("token", token)
		},
    // 用户信息
    SET_USERINFO: (state, userInfo) =>{
      state.userInfo = userInfo
      sessionStorage.setItem("userInfo", JSON.stringify(userInfo))
    },
    // 侧边导航栏展开与折叠
    SET_COLLAPSE: (state, isCollapse) => {
      state.isCollapse = isCollapse
    }
  },
  actions: {
    getUserInfo({ commit, state }){
      return new Promise((resolve, reject) => {
        loginJs.getUserInfo().then(res => {
          // TODO 角色权限
          commit('SET_USERINFO', res.data.data)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })

    }
  },
  modules: {
    dict: dict
  }
})

4、登录js

import request from '@/axios/request';

const api = {
    getCaptchaImg(data){
        return request.get("captcha", data)
    },
    getUserInfo(data) {
        return request.post("/getUserInfo", data)
    }


}

export default api
  

5、实现效果

在这里插入图片描述

代码地址:

前端: https://gitee.com/longzyl/longzy-vue2
后端: https://gitee.com/longzyl/longzy-admin

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值