使用SpringBoot
初始化器创建SpringBoot
项目
修改项目依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.9version>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>baizhi-securityartifactId>
<version>0.0.1-SNAPSHOTversion>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.15version>
dependency>
<dependency>
<groupId>com.github.pengglegroupId>
<artifactId>kaptchaartifactId>
<version>2.3.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
Java
环境
JDK 1.8 |
---|
YAML
配置
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: root
redis:
host: 192.168.47.128 # 虚拟机 ip
port: 6379 # (配置过主从复制)必须使用 master 机器 的端口号
database: 0 # 选择的数据库实例
connect-timeout: 10000 # 超时时间
mybatis:
type-aliases-package: com.example.baizhisecurity.entity
mapper-locations: com/example/baizhisecurity/mapper/*Mapper.xml
logging:
level:
com.example.baizhisecurity: debug # 查看 SQL
# 修改服务器的过期时间为 1 分钟
server:
servlet:
session:
timeout: 1
error: # 自定义错误页面相关的配置
whitelabel:
enabled: false # 关闭默认的显示
path: /error # 定义错误的路径
resources: # 资源映射
add-mappings: true
user
user |
---|
-- {noop} 是 SpringSecurity 密码无加密的 id
INSERT INTO `user` VALUES (1, 'root', '{bcrypt}$2a$10$f1Y3k626cs1ict.wKKWNDuFwk46.YkcdIx/Ib/wHEsnoW7Uo/1Nb6', 1, 1, 1, 1);
INSERT INTO `user` VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (3, 'coder-itl', '{noop}123', 1, 1, 1, 1);
role
role |
---|
INSERT INTO `security`.`role` (`id`, `name`, `name_zh`) VALUES (1, 'ROLE_product', '商品管理员');
INSERT INTO `security`.`role` (`id`, `name`, `name_zh`) VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `security`.`role` (`id`, `name`, `name_zh`) VALUES (3, 'ROLE_user', '用户管理员');
user_role
INSERT INTO `security`.`user_role` (`id`, `uid`, `rid`) VALUES (1, 1, 1);
INSERT INTO `security`.`user_role` (`id`, `uid`, `rid`) VALUES (2, 1, 2);
INSERT INTO `security`.`user_role` (`id`, `uid`, `rid`) VALUES (3, 2, 2);
INSERT INTO `security`.`user_role` (`id`, `uid`, `rid`) VALUES (4, 3, 3);
用户实体
package com.example.baizhisecurity.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
// 权限集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
角色实体
package com.example.baizhisecurity.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
private Integer id;
private String name;
private String nameZh;
}
测试控制器类
@RestController
public class HelloController {
@GetMapping("/hello")
public ResultModel hello() {
return ResultModel.success(HttpStatus.OK.value(), "访问成功", "Hello developer,You successfully retrieved the data!");
}
}
响应
public class ResponseUtil {
public static void out(HttpServletResponse response,ResultModel resultModel){
ObjectMapper objectMapper = new ObjectMapper();
// 设置响应的状态为 200
response.setStatus(HttpStatus.OK.value());
// 设置响应的格式为 JSON 格式
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
try {
// 使用jackson,把json格式的resultModel写入到response的输出流中
objectMapper.writeValue(response.getOutputStream(),resultModel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
统一数据返回模型
package com.example.baizhisecurity.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResultModel<T> implements Serializable {
// 状态码
private int code; // 1000表示成功 401 表示认证失败
// 消息
private String message;
// 数据
private T data;
private static ResultModel resultModel = new ResultModel();
public static ResultModel success(String message) {
resultModel.setCode(1000);
resultModel.setMessage(message);
resultModel.setData(null);
return resultModel;
}
public static ResultModel success(Object data) {
resultModel.setCode(1000);
resultModel.setMessage("success");
resultModel.setData(data);
return resultModel;
}
public static ResultModel success(String message, Object data) {
resultModel.setCode(1000);
resultModel.setMessage(message);
resultModel.setData(data);
return resultModel;
}
public static ResultModel success(Integer code, String message) {
resultModel.setCode(1000);
resultModel.setMessage(message);
return resultModel;
}
public static ResultModel success(Integer code, String message, Object data) {
resultModel.setCode(code);
resultModel.setMessage(message);
resultModel.setData(data);
return resultModel;
}
public static ResultModel error() {
resultModel.setCode(500);
resultModel.setMessage("error");
return resultModel;
}
public static ResultModel error(int code, String message) {
resultModel.setCode(code);
resultModel.setMessage(message);
return resultModel;
}
}
配置类的实现
package com.example.baizhisecurity.config;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Slf4j
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// redis
private final StringRedisTemplate redisTemplate;
// 登录成功处理
private final MyLogoutSuccessHandler myLogoutSuccessHandler;
// 自定义认证成功处理
private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
// 自定义认证失败处理
private final MyAuthenticationFailureHandler myAuthenticationFailureHandler;
// 自定义认证异常处理
private final MyAuthenticationEntryPoint myAuthenticationEntryPoint;
// RememberMe 需要的数据源
private final DataSource dataSource;
// 数据库数据源认证
private final MyUserDetalService myUserDetalService;
// 自定义授权异常处理
private final MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
public SecurityConfig(
DataSource dataSource,
StringRedisTemplate redisTemplate,
MyUserDetalService myUserDetalService,
MyAccessDeniedHandler myAccessDeniedHandler,
MyLogoutSuccessHandler myLogoutSuccessHandler,
MyAuthenticationEntryPoint myAuthenticationEntryPoint,
MyAuthenticationFailureHandler myAuthenticationFailureHandler,
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler
) {
this.redisTemplate = redisTemplate;
this.myLogoutSuccessHandler = myLogoutSuccessHandler;
this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler;
this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
this.myAuthenticationEntryPoint = myAuthenticationEntryPoint;
this.dataSource = dataSource;
this.myUserDetalService = myUserDetalService;
this.myAccessDeniedHandler = myAccessDeniedHandler;
}
// 放行资源白名单
private static final String[] WHITE = {
"/login",
"/css/**",
"/img/**",
"/captcha/**"
};
/**
* TODO: 自定义前后端分离 Form 表单 => JSON 格式
* 自定义 Filter 交给工厂管理
*/
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter(redisTemplate);
// 设置认证路径
loginFilter.setFilterProcessesUrl("/login");
// 指定接受 json 用户名的 key
loginFilter.setUsernameParameter("username");
// 指定接受 json 密码的 key
loginFilter.setPasswordParameter("password");
// 指定接受 json 验证码的 key
loginFilter.setKaptchaParameter("kaptcha");
// 指定接受 json 记住我的 key
loginFilter.setRememberMeParameter("remember-me");
// TODO 什么作用
loginFilter.setAuthenticationManager(authenticationManagerBean());
// 认账成功处理
loginFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
//认证失败处理
loginFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
// TODO 设置认证成功时使用自定义 rememberMeService
loginFilter.setRememberMeServices(rememberMeServices());
return loginFilter;
}
/**
* authenticationManagerBean 是一个方法名,用于获取一个 Spring Security 的认证管理器实例,
* 该方法将认证管理器实例化并将其注入到 Spring 上下文中以供其他 Bean 使用。
* Spring Security 默认会为您提供一个认证管理器实例,但如果您需要在自己的代码中使用它,
* 可以使用这个方法将其注入到您的代码中。
* 在这个方法中,super.authenticationManagerBean() 调用了父类的同名方法,
* 返回了一个 AuthenticationManager 实例。这个实例将被 Spring 管理并注入到上下文中。
* Regenerate response
*
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 自定义 AuthenticationManager 推荐
* 它的作用是管理用户认证的过程。
* 具体来说,它接收用户的登录请求并从Spring Security进行用户认证。在进行用户认证的过程中,AuthenticationManager 首先根据用户名获取用户信息,
* 然后将给定的用户名和密码与用户信息进行比较,如果验证通过,则认为用户已经被认证。如果验证失败,则会抛出异常,表示用户认证失败。
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetalService);
}
/**
* 前后端分离的配置实现
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 前后端分离配置开启 csrf
.csrf()
// 将令牌保存到 cookie 中,允许 cookie 前端获取
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
// 放行资源
.authorizeRequests().mvcMatchers(WHITE).permitAll()
// 认证资源
.anyRequest().authenticated()
// 开启表单认证
.and()
.formLogin()
.and()
// 注销
.logout()
// 前后端分离的处理方式,页面不跳转,响应 json 格式
.logoutSuccessHandler(myLogoutSuccessHandler)
// 清除会话、清楚认证标记、注销成功后的默认跳转到登录页等为默认配置,可以不声明出现
// 退出的请求方式指定 GET、POST
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout", "GET"),
// 可以指定多种同时指定请求方式
new AntPathRequestMatcher("/myLogout", "POST")
))
.and()
// 认证异常的处理
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
// 授权异常处理
.accessDeniedHandler(myAccessDeniedHandler)
// 记住我
.and()
.rememberMe()
// 前后端分离的实现: 设置自动登录使用那个 rememberMe
.rememberMeServices(rememberMeServices())
// 跨域配置,当加入 SpringSecurity 后,原来SpringBoot的跨域解决失效
.and()
.cors()
;
// at: 用来某个 filter 替换过滤器链中那个 filter
// before: 放在过滤器链中那个 filter 之前
// after: 放在过滤器链中那个 filter 之后
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
// 指定 RememberMe 数据持久化处理
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 指定数据源
tokenRepository.setDataSource(dataSource);
// TODO 第一次使用需要设置为 true
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
/**
* 前后端分离记住我的实现
*
* @return MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository)
*/
@Bean
public RememberMeServices rememberMeServices() {
return new MyRememberServices(UUID.randomUUID().toString(), userDetailsService(), persistentTokenRepository());
}
}
自定义授权异常处理
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseUtil.out(response, ResultModel.error(HttpStatus.FORBIDDEN.value(), "请获取授权后在访问...."));
}
}
自定义认证异常处理
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, ResultModel.error(HttpStatus.UNAUTHORIZED.value(), "请认证之后再去处理...."));
}
}
自定义认证失败处理
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ResponseUtil.out(response, ResultModel.error(HttpStatus.UNAUTHORIZED.value(), "认证失败"));
}
}
自定义认证成功后的处理
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ResponseUtil.out(response, ResultModel.success(HttpStatus.OK.value(), "认证成功", authentication));
}
}
自定义注销成功的处理
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ResponseUtil.out(response, ResultModel.success(HttpStatus.OK.value(), "注销成功"));
}
}
自定义前后端分离认证 Filter
package com.example.baizhisecurity.filter;
import com.example.baizhisecurity.exception.KaptchaNotMatchException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* 自定义前后端分离认证 Filter
*/
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private StringRedisTemplate redisTemplate;
// 设置默认的表单验证码 name = kaptcha
private static final String FORM_KAPTCHA_KEY = "kaptcha";
private static final String FORM_REMEMBER_ME_KEY = "remember-me";
private String kaptchaParameter = FORM_KAPTCHA_KEY;
private String rememberMeParameter = FORM_REMEMBER_ME_KEY;
public LoginFilter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 提供自定义的验证码名称
public String getKaptchaParameter() {
return this.kaptchaParameter;
}
public void setKaptchaParameter(final String kaptchaParameter) {
this.kaptchaParameter = kaptchaParameter;
}
public String getRememberMeParameter() {
return rememberMeParameter;
}
public void setRememberMeParameter(String rememberMeParameter) {
this.rememberMeParameter = rememberMeParameter;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 1. 判断请求方式是否是 POST
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断 数据是否是 JSON 格式
ServletRequest re = (ServletRequest) request;
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
// 将请求体中的数据进行反序列化
Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
// 获取 json 用户名
String username = userInfo.get(getUsernameParameter());
// 获取 json 密码
String password = userInfo.get(getPasswordParameter());
// 获取 json 验证码
String kaptcha = userInfo.get(getKaptchaParameter());
// 获取 session 中的验证码
String redisCode = redisTemplate.opsForValue().get("kaptcha");
log.info("redisCode: {}", redisCode);
// 获取 json 中的记住我
String rememberMe = userInfo.get(getRememberMeParameter());
if (!ObjectUtils.isEmpty(rememberMe)) {
// 将这个 remember-me 设置到作用域中
request.setAttribute(getRememberMeParameter(), rememberMe);
}
// 用户输入的验证码和 session 作用域中的都不能为空
if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(redisCode) && kaptcha.equalsIgnoreCase(redisCode)) {
log.info("用户名: {} 密码: {},是否记住我: {}", userInfo, password, rememberMe);
// 获取用户名和密码认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 没有通过则执行自定义异常
throw new KaptchaNotMatchException("验证码不匹配!");
}
// 如果不是 JSON 格式数据,则调用传统方式进行认证
return super.attemptAuthentication(request, response);
}
}
实现
package com.example.baizhisecurity.config.rememberme;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
/**
* TODO 这个类不能被 Spring 容器管理
* 自定义记住我 service 的实现,这个类必须实现它的构造方法
*/
public class MyRememberServices extends PersistentTokenBasedRememberMeServices {
public MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
/**
* 自定义前后端分离获取 remember-me 的方式
*
* @param request
* @param parameter
* @return
*/
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
// 获取作用域中存储的 String rememberMe =
Object parameterRememberMe = request.getAttribute(parameter);
if (!ObjectUtils.isEmpty(parameterRememberMe)) {
String rememberMe = parameterRememberMe.toString();
if (rememberMe == null || !rememberMe.equalsIgnoreCase("true") && !rememberMe.equalsIgnoreCase("on") && !rememberMe.equalsIgnoreCase("yes") && !rememberMe.equals("1")) {
this.logger.debug(LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", parameter));
return false;
} else {
return true;
}
}
// 进行传统表单认证
return super.rememberMeRequested(request, parameter);
}
}
这个地方很特殊,在看到的教学过程中会在当前类下创建一个配置类,设置为数据源,但在这个项目学习的过程中出现了意外的错误CORS error
,在这个过程中,预检
请求发送成功,但是到了最真实的请求时,就出现错误,经过不断地修改跨域配置,前期在Vue
项目中添加了devServer
配置,对于跨域同样是失效的。
// http 此种配置可能未生效在前后端分离中,但是之前使用的时候是生效的,这个点暂时属于疑问,希望多多评论
http.cors().configurationSource(configurationSource())
// SpringSecurity 配置后未能生效的跨域配置
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
真实有效的解决方案
package com.example.baizhisecurity.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 1. 先对 SpringBoot 配置,运行跨域请求
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许 Cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的 header 属性
.allowedHeaders("*")
// 设置允许时间
.maxAge(3600L);
}
}
// 2. 最后只需要在 SpringSecurity 的 hppt 配置跨域
http.cors();
在经过上面两步后,成功解决
CORS
引起的问题并成功的获取到了数据。
配置验证码
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "120");
properties.setProperty("kaptcha.image.height", "40");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOASDFGHJKLZXCVBNM");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
验证码的控制器类
@Slf4j
@CrossOrigin
@RestController
public class CaptchaController {
@Autowired
private Producer producer;
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/captcha")
public ResultModel getVerifyCode(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
// 1. 生成验证码
String text = producer.createText();
log.info("code text: {}", text);
// 2. TODO 放入 session/redis
redisTemplate.opsForValue().set("kaptcha", text);
// 3. 生成图片
BufferedImage image = producer.createImage(text);
FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", fos);
String base64Img = Base64.encodeBase64String(fos.toByteArray());
return ResultModel.success(HttpStatus.OK.value(), "验证码获取成功!", base64Img);
}
}
验证码异常
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg, Throwable cause) {
super(msg, cause);
}
public KaptchaNotMatchException(String msg) {
super(msg);
}
}
全局异常处理
@ControllerAdvice
public class GlobalExceptionHandle {
@ResponseBody
@ExceptionHandler(Exception.class)
public ResultModel error(Exception e) {
e.printStackTrace();
return ResultModel.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "执行了全局异常处理!");
}
}
Mapper
定义
@Repository
public interface UserMapper {
User findUserByUserName(String username);
List<Role> getRoleByUid(Integer uid);
Integer updatePassword(String username, @Param("password") String newPassword);
}
Mapper
映射实现
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.baizhisecurity.mapper.UserMapper">
<select id="findUserByUserName" resultType="user">
select *
from user
where username = #{username}
select>
<select id="getRoleByUid" resultType="role">
select r.id, r.name, r.name_zh
from role r,
user_role ur
where r.id = ur.uid
and ur.uid = #{uid}
select>
<update id="updatePassword">
update `user`
set password = #{password}
where username = #{username}
update>
mapper>
UserDetailsService
package com.example.baizhisecurity.service;
import com.example.baizhisecurity.entity.Role;
import com.example.baizhisecurity.entity.User;
import com.example.baizhisecurity.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.List;
@Service
public class MyUserDetalService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户
User user = userMapper.findUserByUserName(username);
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 查询用户的权限信息
// 查询权限信息
List<Role> roles = userMapper.getRoleByUid(user.getId());
user.setRoles(roles);
return user;
}
/**
* 自动密码升级解决方案 {推荐: 随着 SpringSecurity 版本的升级,密码的底层加密会实现自动升级}
*
* @param user
* @param newPassword
* @return
*/
// 实现密码更新
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer updatePassword = userMapper.updatePassword(user.getUsername(), newPassword);
if (updatePassword == 1) {
((User) user).setPassword(newPassword);
}
return user;
}
}
ElemenUI
选择了全局安装
登录表单
<template>
<div class="login" v-cloak>
<div class="left">
<video autoplay="autoplay" loop="loop" muted oncanplay="true" src="@/assets/video/passport.mp4">video>
div>
<div class="right">
<div class="box">
<p>
<strong> 登录 strong>
<span>没有账户? <router-link to="/register">免费注册router-link>
span>
p>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="" prop="username">
<el-input placeholder="请输入账号" v-model="ruleForm.username" type="text">
<i slot="suffix" class="el-input__icon icon-jurassic_user">i>
el-input>
el-form-item>
<el-form-item label="" prop="password">
<el-input v-model="ruleForm.password" ref="pwdRef" placeholder="请输入密码" :type="inputType">
<i slot="suffix" class="el-input__icon icon-mima" @click="showPasswd">i>
el-input>
el-form-item>
<el-form-item label="" prop="kaptcha" class="code">
<el-input placeholder="请输入验证码" v-model="ruleForm.kaptcha" type="text" style="width: 170px;">
<i slot="suffix" class="el-input__icon icon-yanzhengma">i>
el-input>
<img :src="kaptchaCode" ref="captchaImg" alt="" title="点击刷新" @click="refreshCaptcha">
el-form-item>
<el-button @click="loginHandle">登录el-button>
el-form>
div>
div>
div>
template>
<script>
import { loginNetwork, refNewCode } from "@/network/user/user";
export default {
data() {
return {
ruleForm: {
username: '', // 用户名
password: '', // 密码
kaptcha: '' // 验证码
},
kaptchaCode: "",
showPassword: false, // 默认不显示密码
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
min: 3,
max: 15,
message: '长度在 3 到 15 个字符',
trigger: 'blur',
},
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
min: 3,
max: 15,
message: '长度在 3 到 15 个字符',
trigger: 'blur',
},
],
kaptcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{
min: 3,
max: 5,
message: '长度在 4 个字符',
trigger: 'blur',
},
],
},
}
},
computed: {
// 修改密码显示
inputType() {
return this.showPassword ? 'text' : 'password';
},
},
created() {
this.refreshCaptcha()
},
methods: {
// 点击刷新验证码
refreshCaptcha() {
refNewCode().then(res => {
if (res.code === 200) {
// 解析 base64 图片资源 data:image/png;base64,
this.kaptchaCode = "data:image/png;base64," + res.data
this.$message.success(res.message || "刷新成功!")
} else {
this.$message.error(res.message || "验证码获取失败!")
}
})
},
// 点击显示验证码明文字符
showPasswd() {
this.showPassword = !this.showPassword;
},
// 点击登录事件
loginHandle() {
// 表单校验
this.$refs.ruleForm.validate((valid) => {
if (valid) {
console.log(valid)
loginNetwork(this.ruleForm).then(res => {
console.log("loginNetwork: ", res)
// 判断 code
if (res.code === 200) {
this.$message.success(res.message)
// TODO 页面跳转
this.$router.push("/admin")
} else {
this.$message.error(res.message)
}
})
}
})
}
},
}
script>
<style lang="less" scoped>
[v-cloak] {
display: none;
}
.code {
display: flex;
justify-content: space-between;
align-items: center;
img {
height: 40px;
line-height: 40px;
margin-left: 10px;
vertical-align: middle;
}
}
.icon-yanjing_xianshi {
position: absolute;
font-size: 14px;
z-index: 1;
right: 10px;
color: #606266;
font-family: iconfont;
}
.el-button:hover {
background: #ffa459;
}
.icon-mima,
.icon-yanzhengma,
.icon-jurassic_user {
font-family: iconfont;
}
.box p {
position: relative;
left: 80px;
padding: 20px;
strong {
font-size: 32px;
font-weight: 600;
line-height: 40px;
color: #121315;
}
span {
display: block;
margin-top: 8px;
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: #767e89;
}
a {
color: #fb9337;
cursor: pointer;
transition: color 0.3s;
}
}
.right {
position: relative;
width: 50%;
margin-left: 140px;
box-sizing: border-box;
.box {
position: absolute;
top: 300px;
}
.el-form {
width: 100%;
.el-input {
width: 300px;
}
}
}
.el-button {
position: relative;
left: 100px;
width: 300px;
color: #fff;
background-color: #fb9337;
}
.login {
display: flex;
justify-content: space-between;
width: 100%;
height: 100%;
.left video {
display: inline-block;
width: 100%;
height: 100vh;
object-fit: cover;
}
}
style>
发送请求认证测试
表单测试 |
---|
源代码下载
https://gitee.com/coderitl/split-springsecurity.git
JSON
格式显示WEB
开发,而前后端分离是将登陆表单以JSON
格式显示的测试获取验证码
http://localhost:8080/captcha
data 是图片数据的Base64 显示,前端是需要拼接的 |
POSTMAN 测试 |
---|---|
测试直接访问控制器数据
未登陆时访问数据 | |
---|---|
细节
这里需要注意,使用的时候需要在header
中添加CSRF
需要的键值
第一步获取cookie 中关于CSRF 相关的键值 |
---|
将上图中红色框
中的值复制下来,添加到本次请求的header
中
CSRF 配置 |
---|
在添加好后,再次访问请求
成功获得认证 |
---|
下次访问时,需要删除header
中CSRF
的值,之后再次添加
疑问点难道每一次都需要访问一次失败的再添加cookie后才能访问成功吗?
在前端使用的时候,是通过添加相关的配置获取的是
cookie
的内容,所以访问时就已经实现了添加,所以不会出现访问失败一次的现象。
Vue
中CSRF
的配置
下载插件
# 下载 cookie 使用的插件
npm install vue-cookie --save
使用
// config.js
import axios from "axios";
import VueCookie from "vue-cookie";
axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN";
axios.defaults.xsrfCookieName = "CSRF-TOKEN";
axios.defaults.withCredentials = "true";
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: "http://localhost:8080",
timeout: 5000,
});
// 2.axios的拦截器
// 2.1.请求拦截的作用
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
// 获取 CSRF Token
const csrfToken = VueCookie.get("XSRF-TOKEN");
console.log("csrfToken: " + csrfToken);
if (csrfToken) {
// 在请求头中添加 CSRF Token
config.headers["X-XSRF-TOKEN"] = csrfToken;
}
return config;
},
(err) => {
// 对请求错误做些什么
return Promise.reject(err);
}
);
// 2.2.响应拦截
instance.interceptors.response.use(
(res) => {
return res.data;
},
(err) => {
console.log(err);
}
);
// 3.发送真正的网络请求
return instance(config);
}