> 文章列表 > 前后端分离下的-SpringSecurity

前后端分离下的-SpringSecurity

前后端分离下的-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
    前后端分离下的-SpringSecurity
  • 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
    前后端分离下的-SpringSecurity
    -- {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
    前后端分离下的-SpringSecurity
    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

    前后端分离下的-SpringSecurity
    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>
    
    • 渲染效果

      登录页面
      前后端分离下的-SpringSecurity
  • 发送请求认证测试

    表单测试
    前后端分离下的-SpringSecurity

代码下载

  • 源代码下载

    https://gitee.com/coderitl/split-springsecurity.git

特殊点说明

  • 项目整体采用的是前后端分离开发
  • 前后端分离后的特点是所有响应以JSON格式显示
  • 在登录页面上,需要特别的注意自定义登录页面是针对传统的WEB开发,而前后端分离是将登陆表单以JSON格式显示的

项目测试

  • 测试获取验证码

    http://localhost:8080/captcha

    data是图片数据的Base64显示,前端是需要拼接的 POSTMAN测试
    前后端分离下的-SpringSecurity 前后端分离下的-SpringSecurity
  • 测试直接访问控制器数据

    未登陆时访问数据
    前后端分离下的-SpringSecurity
    • 细节

      1. 这里需要注意,使用的时候需要在header中添加CSRF需要的键值

        第一步获取cookie中关于CSRF相关的键值
        前后端分离下的-SpringSecurity
      2. 将上图中红色框中的值复制下来,添加到本次请求的header

        CSRF配置
        前后端分离下的-SpringSecurity
      3. 在添加好后,再次访问请求

        成功获得认证
        前后端分离下的-SpringSecurity
      4. 下次访问时,需要删除headerCSRF的值,之后再次添加

      5. 疑问点难道每一次都需要访问一次失败的再添加cookie后才能访问成功吗?

        在前端使用的时候,是通过添加相关的配置获取的是cookie的内容,所以访问时就已经实现了添加,所以不会出现访问失败一次的现象。

      6. VueCSRF的配置

        • 下载插件

          # 下载 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);
          }