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