前后端分离下的-SpringSecurity
前后端分离下的SpringSecurity
项目创建
-
使用
SpringBoot
初始化器创建SpringBoot
项目 -
修改项目依赖
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.9</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>baizhi-security</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.15</version></dependency><!-- 验证码 --><dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
-
Java
环境JDK 1.8
-
YAML
配置spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTCusername: rootpassword: rootredis:host: 192.168.47.128 # 虚拟机 ipport: 6379 # (配置过主从复制)必须使用 master 机器 的端口号database: 0 # 选择的数据库实例connect-timeout: 10000 # 超时时间mybatis:type-aliases-package: com.example.baizhisecurity.entitymapper-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<>();// 权限集合@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Set<SimpleGrantedAuthority> authorities = new HashSet<>();roles.forEach(role -> {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());authorities.add(simpleGrantedAuthority);});return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic 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!");} }
JSON 响应和统一数据返回
-
响应
public class ResponseUtil {public static void out(HttpServletResponse response,ResultModel resultModel){ObjectMapper objectMapper = new ObjectMapper();// 设置响应的状态为 200response.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;} }
SpringSecurity 的配置
配置类
-
配置类的实现
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 {// redisprivate 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;@Autowiredpublic 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 交给工厂管理*/@Beanpublic LoginFilter loginFilter() throws Exception {LoginFilter loginFilter = new LoginFilter(redisTemplate);// 设置认证路径loginFilter.setFilterProcessesUrl("/login");// 指定接受 json 用户名的 keyloginFilter.setUsernameParameter("username");// 指定接受 json 密码的 keyloginFilter.setPasswordParameter("password");// 指定接受 json 验证码的 keyloginFilter.setKaptchaParameter("kaptcha");// 指定接受 json 记住我的 keyloginFilter.setRememberMeParameter("remember-me");// TODO 什么作用loginFilter.setAuthenticationManager(authenticationManagerBean());// 认账成功处理loginFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);//认证失败处理loginFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);// TODO 设置认证成功时使用自定义 rememberMeServiceloginFilter.setRememberMeServices(rememberMeServices());return loginFilter;}/* authenticationManagerBean 是一个方法名,用于获取一个 Spring Security 的认证管理器实例,* 该方法将认证管理器实例化并将其注入到 Spring 上下文中以供其他 Bean 使用。* Spring Security 默认会为您提供一个认证管理器实例,但如果您需要在自己的代码中使用它,* 可以使用这个方法将其注入到您的代码中。* 在这个方法中,super.authenticationManagerBean() 调用了父类的同名方法,* 返回了一个 AuthenticationManager 实例。这个实例将被 Spring 管理并注入到上下文中。* Regenerate response @return* @throws Exception*/@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/* 自定义 AuthenticationManager 推荐* 它的作用是管理用户认证的过程。* 具体来说,它接收用户的登录请求并从Spring Security进行用户认证。在进行用户认证的过程中,AuthenticationManager 首先根据用户名获取用户信息,* 然后将给定的用户名和密码与用户信息进行比较,如果验证通过,则认为用户已经被认证。如果验证失败,则会抛出异常,表示用户认证失败。 @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetalService);}/* 前后端分离的配置实现 @param http* @throws Exception*/@Overrideprotected 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 数据持久化处理@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();// 指定数据源tokenRepository.setDataSource(dataSource);// TODO 第一次使用需要设置为 truetokenRepository.setCreateTableOnStartup(false);return tokenRepository;}/* 前后端分离记住我的实现 @return MyRememberServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository)*/@Beanpublic RememberMeServices rememberMeServices() {return new MyRememberServices(UUID.randomUUID().toString(), userDetailsService(), persistentTokenRepository());} }
前后端分离相关自定义实现
-
自定义授权异常处理
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 = kaptchaprivate 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;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {// 1. 判断请求方式是否是 POSTif (!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*/@Overrideprotected 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 {@Overridepublic 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 {@Beanpublic 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 {@Autowiredprivate Producer producer;@Autowiredprivate 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/redisredisTemplate.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 定义
-
Mapper
定义@Repository public interface UserMapper {User findUserByUserName(String username);List<Role> getRoleByUid(Integer uid);Integer updatePassword(String username, @Param("password") String newPassword); }
-
Mapper
映射实现<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.baizhisecurity.mapper.UserMapper"><!-- User findUserByUserName(String username); --><select id="findUserByUserName" resultType="user">select *from userwhere username = #{username}</select><!-- List<Role> getRoleByUid(Integer uid); --><select id="getRoleByUid" resultType="role">select r.id, r.name, r.name_zhfrom role r,user_role urwhere r.id = ur.uidand ur.uid = #{uid}</select><!-- Integer updatePassword(@Param("username") String username,@Param("password") String password);--><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 {@Autowiredprivate UserMapper userMapper;@Overridepublic 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*/// 实现密码更新@Overridepublic 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.datathis.$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)// 判断 codeif (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 Tokenconst csrfToken = VueCookie.get("XSRF-TOKEN");console.log("csrfToken: " + csrfToken);if (csrfToken) {// 在请求头中添加 CSRF Tokenconfig.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); }
-
-
-