【业务功能篇60】Springboot + Spring Security 权限管理 【终篇】

news/2024/4/20 12:26:57/文章来源:https://blog.csdn.net/studyday1/article/details/131996303

4.4.7 权限校验扩展

4.4.7.1 @PreAuthorize注解中的其他方法

  • hasAuthority:检查调用者是否具有指定的权限;
 @RequestMapping("/hello")@PreAuthorize("hasAuthority('system:user:list')")public String hello(){return "hello Spring Security! !";}
  • hasAnyAuthority:检查调用者是否具有指定的任何一个权限;
 @RequestMapping("/ok")@PreAuthorize("hasAnyAuthority('system:user:list,system:role:list')")public String ok(){return "ok Spring Security! !";}
  • hasRole:检查调用者是否有指定的角色;

**hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 **ROLE_ 后再去比较。所以这种情况下要用户对应的权限也要有 ROLE_ 这个前缀才可以。

 @RequestMapping("/level1")@PreAuthorize("hasRole('admin')")public String level1(){return "level1 page";}
  • hasAnyRole:检查调用者是否具有指定的任何一个角色;
 @RequestMapping("/level2")@PreAuthorize("hasAnyRole('admin','common')")public String level2(){return "level2 page";}

4.4.7.2 权限校验源码分析

  • 详见视频

4.4.7.3 自定义权限校验

我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。

 /*** 自定义权限校验方法* @author spikeCong* @date 2023/4/27**/@Component("my_ex")public class MyExpression {/*** 自定义 hasAuthority* @param authority 接口指定的访问权限限制* @return: boolean*/public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> permissions = loginUser.getPermissions();//判断集合中是否有authorityreturn permissions.contains(authority);}}

使用SPEL表达式,引入自定义的权限校验

SPEL(Spring Expression Language)是 Spring 框架提供的一种表达式语言,用于在 Spring 应用程序中进行编程和配置时使用。

Spring Security 中的权限表达式:可以使用 SPEL 表达式定义在授权过程中使用的逻辑表达式

 @RequestMapping("/ok")@PreAuthorize("@my_ex.hasAuthority('system:role:list')")public String ok(){return "ok";}

4.4.7.4 基于配置的权限控制

  • 在security配置类中,通过配置的方式对资源进行权限控制
 @RequestMapping("/yes")public String yes(){return "yes";}
    @Overrideprotected void configure(HttpSecurity http) throws Exception {//关闭csrfhttp.csrf().disable();//允许跨域http.cors();http    //不会创建会话,每个请求都将被视为独立的请求。.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//定义请求授权规则.authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()//配置形式的权限控制.antMatchers("/yes").hasAuthority("system/menu/index")// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//将自定义认证过滤器,添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling()//配置认证失败处理器.authenticationEntryPoint(authenticationEntryPoint)//配置授权失败处理器.accessDeniedHandler(accessDeniedHandler);}

4.4.7.5 角色加权限校验方式解析

(1) Role 和 Authority 的区别

用户拥有的权限,有以下两种表示

 roles("admin","common","test")authorities("system:user:list","system:role:list","system:menu:list");

给资源授予权限(角色或权限)

 @PreAuthorize("hasAuthority('system:user:list')")@PreAuthorize("hasAnyAuthority('system:user:list,system:role:list')")@PreAuthorize("hasRole('admin')")@PreAuthorize("hasAnyRole('admin','common')")

用户权限的保存方式

  • roles("admin","common","test"),增加”ROLE“前缀存放:

    • 【“ROLE_admin”,“ROLE_common”,"ROLE_test"】 表示拥有的权限。
    • 一个角色表示的是多个权限,用户传入的角色不能以 ROLE开头,否则会报错。ROLE是自动加上的 如果我们保存的用户的角色:直接传入角色的名字,权限【new SimpleGrantedAuthority(“ROLE“ + role)】保存即可
  • authorities (“USER”,”MANAGER”),原样存放:

    • 【"system:user:list","system:role:list"】 表示拥有的权限。
    • 如果我们保存的是真正的权限;直接传入权限名字,权限【new SimpleGrantedAuthority(permission)】保存

**无论是 Role 还是 Authority 都保存在 **List<GrantedAuthority>,每个用户都拥有自己的权限集合

用户权限的验证方式

  • 通过角色(权限)验证: 拥有任何一个角色都可以访问,验证时会自动增加”ROLE_“进行查找验证:【”ROLE_admin”,”ROLE_common”】
  • **通过权限验证: ** 拥有任何一个权限都可以访问,验证时原样查找进行验证:【”system:role:list”】
(2) 结合角色进行权限控制
  • 创建Role角色实体
 @Data@AllArgsConstructor@NoArgsConstructor@TableName(value = "sys_role")@JsonInclude(JsonInclude.Include.NON_NULL)public class Role implements Serializable {@TableIdprivate Long roleId;/*** 角色名*/private String roleName;/*** 角色权限字符串*/private String roleKey;/*** 角色状态 0正常,1停用*/private String status;/*** 删除标志 0存在,1删除*/private String delFlag;private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;private String remark;}
  • RoleMapper
 public interface RoleMapper  extends BaseMapper<Role> {List<String> selectRolesByUserId(Long id);}
 <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mashibing.springsecurity_example.mapper.RoleMapper"><select id="selectRolesByUserId" resultType="java.lang.String">SELECTsr.role_keyFROM sys_user_role surLEFT JOIN sys_role sr ON sur.role_id = sr.role_idWHERE sur.user_id = #{userid} AND sr.status = 0 AND sr.del_flag = 0</select></mapper>
  • UserServiceDetailsImpl
 @Datapublic class LoginUser implements UserDetails {private SysUser sysUser;public LoginUser() {}public LoginUser(SysUser sysUser) {this.sysUser = sysUser;}//存储权限信息集合private List<String> permissions;//存储角色信息集合private List<String> roles;public LoginUser(SysUser user, List<String> permissions) {this.sysUser = user;this.permissions = permissions;}public LoginUser(SysUser user, List<String> permissions, List<String> roles) {this.sysUser = user;this.permissions = permissions;this.roles = roles;}//避免出现异常@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities;/***  用于获取用户被授予的权限,可以用于实现访问控制。*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if(authorities != null){return authorities;}//1.8 语法authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());//处理角色信息authorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());return authorities;}}
  • Controller
 @RequestMapping("/level1")//当前用户是common角色,并且具有system:role:list或者system:user:list@PreAuthorize("hasRole('common') AND hasAnyAuthority('system:role:list','system:user:list')")public String level1(){return "level1 page";}@RequestMapping("/level2")//当前用户拥有admin或者common角色,或者具有system:role:list权限@PreAuthorize("hasAnyRole('admin','common') OR hasAuthority('system:role:list')")public String level2(){return "level2 page";}
  • 测试一下
 @RequestMapping("/level1")//当前用户是common角色,并且具有system:role:list或者system:user:list@PreAuthorize("hasRole('admin') AND hasAnyAuthority('system:role:list','system:user:list')")public String level1(){return "level1 page";}@RequestMapping("/level2")//当前用户拥有admin或者common角色,或者具有system:role:list权限@PreAuthorize("hasAnyRole('admin','common') OR hasAuthority('system:role:list')")public String level2(){return "level2 page";}

4.4.8 认证方案扩展

我们首先创建一个新的项目,来进行接下来的案例演示,配置文件

 server:#服务器的HTTP端口port: 8888spring:datasource:url: jdbc:mysql://localhost:3306/test_security?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverthymeleaf:prefix: classpath:/templates/suffix: .htmlencoding: UTF-8mode: HTMLcache: falsesecurity:user:name: testpassword: 123456roles: admin,usermybatis-plus:mapper-locations: classpath*:/mapper/**/*.xml

4.4.8.1 自定义认证

(1) 自定义资源权限规则
  1. 引入模板依赖
 <!--thymeleaf--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
  1. 在 templates 中定义登录界面 login.html
 <!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>登录页面</title></head><body><h1>用户登录</h1><form method="post" th:action="@{/login}">用户名:<input name="username" type="text"/><br>密码:<input name="password" type="password"/><br><input type="submit" value="登录"/></form></body></html>
  1. 配置 Spring Security 配置类
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()    //开始配置授权,即允许哪些请求访问系统.mvcMatchers("/login.html").permitAll()   //指定哪些请求路径允许访问.mvcMatchers("/index").permitAll()      //指定哪些请求路径允许访问.anyRequest().authenticated()  //除上述以外,指定其他所有请求都需要经过身份验证.and().formLogin()    //配置表单登录.loginPage("/login.html")      //登录页面.loginProcessingUrl("/login")  //提交路径.usernameParameter("username") //表单中用户名.passwordParameter("password") //表单中密码.successForwardUrl("/index")  //指定登录成功后要跳转的路径为 /index//.defaultSuccessUrl("/index")   //redirect 重定向  注意:如果之前请求路径,会有优先跳转之前请求路径.failureUrl("/login.html") //指定登录失败后要跳转的路径为 /login.htm.and().csrf().disable();//这里先关闭 CSRF}}

说明

  • permitAll() 代表放行该资源,该资源为公共资源 无需认证和授权可以直接访问
  • anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
  • **formLogin() 代表开启表单认证 **
  • successForwardUrl 、defaultSuccessUrl 这两个方法都可以实现成功之后跳转
    • **successForwardUrl 默认使用 **forward跳转 注意:不会跳转到之前请求路径
    • **defaultSuccessUrl 默认使用 **redirect 跳转 注意:如果之前有请求路径,会优先跳转之前请求路径,可以传入第二个参数进行修改

注意: 放行资源必须放在所有认证请求之前!

  1. 创建Controller
 @Controllerpublic class LoginController {@RequestMapping("/ok")public String ok(){return "ok";}@RequestMapping("/login.html")public String login(){return "login";}}
(2) 自定义认证成功处理器
  1. 有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。只需要给前端返回一个 JSON 通知登录成功还是失败与否。这个时候可以通过自定义 AuthenticationSucccessHandler 实现
 public interface AuthenticationSuccessHandler {void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException;}

根据接口的描述信息,也可以得知登录成功会自动回调这个方法,进一步查看它的默认实现,你会发现successForwardUrl、defaultSuccessUrl也是由它的子类实现的

  1. 自定义 AuthenticationSuccessHandler 实现
 @Componentpublic class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}}
  1. 配置 AuthenticationSuccessHandler
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredprivate AuthenticationSuccessHandler successHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()    .and().formLogin()    //配置表单登录.successHandler(successHandler).failureUrl("/login.html") //指定登录失败后要跳转的路径为 /login.htm.and().csrf().disable();//这里先关闭 CSRF}}
  1. 测试一下

image.png

(3) 自定义认证失败处理器
  1. 和自定义登录成功处理一样,Spring Security 同样为前后端分离开发提供了登录失败的处理,这个类就是 AuthenticationFailureHandler,源码为:
 public interface AuthenticationFailureHandler {void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException;}

根据接口的描述信息,也可以得知登录失败会自动回调这个方法,进一步查看它的默认实现,你会发现failureUrl、failureForwardUrl也是由它的子类实现的。

  1. 自定义 AuthenticationFailureHandler 实现
 @Componentpublic class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "登录失败: "+exception.getMessage());result.put("status", 500);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}}
  1. 配置 AuthenticationFailureHandler
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//...failureHandler(new MyAuthenticationFailureHandler()).and().csrf().disable();//这里先关闭 CSRF}}
  1. 测试一下

image.png

(4) 自定义注销登录处理器

Spring Security 中也提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。

  • 开启注销登录 默认开启

     @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//....and().formLogin()//....and().logout().logoutUrl("/logout").invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/login.html").and().csrf().disable();//这里先关闭 CSRF}}
    
    • 通过 logout() 方法开启注销配置
    • **logoutUrl 指定退出登录请求地址,默认是 GET 请求,路径为 **/logout
    • invalidateHttpSession 退出时是否是 session 失效,默认值为 true
    • clearAuthentication 退出时是否清除认证信息,默认值为 true
    • logoutSuccessUrl 退出登录时跳转地址

前后端分离注销登录配置

  • 如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义 LogoutSuccessHandler 实现来返回注销之后信息:
 @Componentpublic class LogoutSuccessHandlerImpl  implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {Map<String, Object> result = new HashMap<String, Object>();result.put("msg", "注销成功");result.put("status", 200);response.setContentType("application/json;charset=UTF-8");String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}}
  • 配置
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredprivate LogoutSuccessHandler logoutSuccessHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()    //开始配置授权,即允许哪些请求访问系统.and().formLogin()    //配置表单登录//....and().logout()//                .logoutUrl("/logout").invalidateHttpSession(true).clearAuthentication(true)//                .logoutSuccessUrl("/login.html").logoutSuccessHandler(logoutSuccessHandler).and().csrf().disable();//这里先关闭 CSRF}}
  • 测试

image.png

4.4.8.2 添加图形验证码

在用户登录时,一般通过表单的方式进行登录都会要求用户输入验证码,Spring Security默认没有实现图形验证码的功能,所以需要我们自己实现。

图形验证码一般是在用户名、密码认证之前进行验证的,所以需要在 UsernamePasswordAuthenticationFilter过滤器之前添加一个自定义过滤器 ImageCodeValidateFilter,用来校验用户输入的图形验证码是否正确。

image.png

自定义的过滤器 ImageCodeValidateFilter 首先会判断请求是否为 POST 方式的登录表单提交请求,如果是就将其拦截进行图形验证码校验。如果验证错误,会抛出自定义异常类对象 ValidateCodeException,该异常类需要继承 AuthenticationException 类。在自定义过滤器中,我们需要手动捕获自定义异常类对象,并将捕获到自定义异常类对象交给自定义失败处理器进行处理。

(1) 传统web开发

Kaptcha 是谷歌提供的生成图形验证码的工具,参考地址为:https://github.com/penggle/kaptcha,依赖如下:

Kaptcha 是一个可高度配置的实用验证码生成工具,可自由配置的选项如:

  1. 验证码的字体
  2. 验证码字体的大小
  3. 验证码字体的字体颜色
  4. 验证码内容的范围(数字,字母,中文汉字!)
  5. 验证码图片的大小,边框,边框粗细,边框颜色
  6. 验证码的干扰线
  7. 验证码的样式(鱼眼样式、3D、普通模糊、…)
  • 引入依赖
 <dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency>
  • 添加验证码配置类
 @Configurationpublic class KaptchaConfig {@Beanpublic Producer kaptcha() {Properties properties = new Properties();// 是否有边框properties.setProperty(Constants.KAPTCHA_BORDER, "yes");// 边框颜色properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");// 验证码图片的宽和高properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "40");// 验证码颜色properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");// 验证码字体大小properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");// 验证码生成几个字符properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");// 验证码随机字符库properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");// 验证码图片默认是有线条干扰的,我们设置成没有干扰properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}}
  • 创建验证码实体类
 public class CheckCode implements Serializable {private String code; //验证字符private LocalDateTime expireTime; //过期时间public CheckCode(String code, int expireTime) {this.code = code;//返回指定的过期时间this.expireTime = LocalDateTime.now().plusSeconds(expireTime);}public CheckCode(String code) {//默认验证码 60秒后过期this(code,60);}//是否过期public boolean isExpired(){return this.expireTime.isBefore(LocalDateTime.now());}public String getCode() {return code;}}
  • 创建生成验证码Controller
 @Controllerpublic class KaptchaController {private final Producer producer;@Autowiredpublic KaptchaController(Producer producer) {this.producer = producer;}@GetMapping("/code/image")public void getVerifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException {//1.创建验证码文本String capText = producer.createText();//2.创建验证码图片BufferedImage bufferedImage = producer.createImage(capText);//3.将验证码文本放进 Session 中CheckCode code = new CheckCode(capText);request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, code);//4.将验证码图片返回,禁止验证码图片缓存response.setHeader("Cache-Control", "no-store");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", 0);//5.设置ContentTyperesponse.setContentType("image/png");ImageIO.write(bufferedImage,"jpg",response.getOutputStream());}}
  • 在 login.html 中添加验证码功能
 <!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>登录</title></head><body><h3>表单登录</h3><form method="post" th:action="@{/login}"><input type="text" name="username" placeholder="用户名"><br><input type="password" name="password" placeholder="密码"><br><input name="imageCode" type="text" placeholder="验证码"><br><img th:onclick="this.src='/code/image?'+Math.random()" th:src="@{/code/image}" alt="验证码"/><br><button type="submit">登录</button></form></body></html>
  • 更改安全配置类 SpringSecurityConfig,设置访问 /code/image不需要任何权限
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeHttpRequests()    //开始配置授权,即允许哪些请求访问系统.mvcMatchers("/login.html","/code/image").permitAll()   //指定哪些请求路径允许访问.anyRequest().authenticated()  //除上述以外,指定其他所有请求都需要经过身份验证.and().formLogin()    //配置表单登录//......}}
  • 测试

访问 http://localhost:8888/login.html,出现图形验证的信息

image.png

  • 创建自定义异常类
 /*** 自定义验证码错误异常* @author spikeCong* @date 2023/4/29**/public class KaptchaNotMatchException extends AuthenticationException {public KaptchaNotMatchException(String msg) {super(msg);}public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}}
  • 自定义图形验证码校验过滤器
 @Componentpublic class KaptchaFilter extends OncePerRequestFilter {//前端输入的图形验证码参数private String codeParameter = "imageCode";//自定义认证失败处理器@Autowiredprivate AuthenticationFailureHandler failureHandler;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {//非post请求的表单提交不校验 图形验证码if (request.getMethod().equals("POST")) {try {//校验图形验证码合法性validate(request);} catch (KaptchaNotMatchException e) {failureHandler.onAuthenticationFailure(request,response,e);return;}}//放行进入下一个过滤器filterChain.doFilter(request,response);}//判断验证码合法性private void validate(HttpServletRequest request) throws KaptchaNotMatchException {//1.获取用户传入的图形验证码值String requestCode = request.getParameter(this.codeParameter);if(requestCode == null){requestCode = "";}requestCode = requestCode.trim();//2.获取session中的验证码值HttpSession session = request.getSession();CheckCode checkCode =(CheckCode) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);if(checkCode != null){//清除验证码,不管成功与否,客户端应该在登录失败后 刷新验证码session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);}// 校验出错,抛出异常if (StringUtils.isBlank(requestCode)) {throw new KaptchaNotMatchException("验证码的值不能为空");}if (checkCode == null) {throw new KaptchaNotMatchException("验证码不存在");}if (checkCode.isExpired()) {throw new KaptchaNotMatchException("验证码过期");}if (!requestCode.equalsIgnoreCase(checkCode.getCode())) {throw new KaptchaNotMatchException("验证码输入错误");}}}
  • 更改安全配置类 SpringSecurityConfig,将自定义过滤器添加过滤器链中
 @Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredprivate AuthenticationSuccessHandler successHandler;@Autowiredprivate AuthenticationFailureHandler failureHandler;@Autowiredprivate LogoutSuccessHandler logoutSuccessHandler;@Autowiredprivate KaptchaFilter kaptchaFilter;/*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {//开启基于HTTP请求访问控制http.authorizeHttpRequests()//开始配置授权,即允许哪些请求访问系统.mvcMatchers("/login.html","/code/image").permitAll()//除上述以外,指定其他所有请求都需要经过身份验证.anyRequest().authenticated();//开启 form表单登录http.formLogin().loginPage("/login.html")      //登录页面(覆盖security的).loginProcessingUrl("/login")  //提交路径.usernameParameter("username") //表单中用户名.passwordParameter("password") //表单中密码// 使用自定义的认证成功和失败处理器.successHandler(successHandler).failureHandler(failureHandler);//开启登出配置http.logout().invalidateHttpSession(true).clearAuthentication(true).logoutSuccessHandler(logoutSuccessHandler);http.csrf().disable();//这里先关闭 CSRF//将自定义图形验证码校验过滤器,添加到UsernamePasswordAuthenticationFilter之前http.addFilterBefore(kaptchaFilter, UsernamePasswordAuthenticationFilter.class);}}
  • 测试

访问 http://localhost:8888/login.html,出现图形验证的信息,输入 用户名密码及 正确验证码

image.png

image.png

访问 localhost:8080/login/page,等待 60 秒后,输入正确的用户名、密码和验证码:

image.png

(3) 前后端分离开发

图形验证码包含两部分:图片和文字验证码。

  • 在JSP时代,图形验证码生成和验证是通过Session机制来实现的:后端生成图片和文字验证码,并将文字验证码放在session中,前端填写验证码后提交到后台,通过与session中的验证码比较来实现验证。
  • 在前后端分离的项目中,登录使用的是Token验证,而不是Session。后台必须保证当前用户输入的验证码是用户开始请求页面时候的验证码,必须保证验证码的唯一性。

前后端分离开发方式保证验证码唯一性的解决思路

  • 把生成的验证码放在全局的的缓存中,如redis,并设置一个过期时间。

  • 前端验证时,需要把验证码的id也带上,供后端验证。

    为每个验证码code分配一个主键codeId。后端接收到获取验证码请求, 生成验证码的同时,生成一个验证码唯一ID, 并且以此唯一ID 为Key 将其保存到redis. 然后响应给前端. 前端请求验证码code时,将codeId在前端生成并发送给后端;后端对code和codeId进行比较,完成验证。

  • 后台在生成图片后使用Base64进行编码

    Base64用于将二进制数据编码成ASCII字符 (图片、文件等都可转化为二进制数据)

1. 回到第一个 springsecurity项目, 先创建一个 CaptchaController

  • **导入easy-captcha **https://gitee.com/ele-admin/EasyCaptcha
         <dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency>
 @RestControllerpublic class CaptchaController {@Autowiredprivate RedisCache redisCache;/*** 生成验证码* @param response* @return: com.mashibing.springsecurity_example.common.ResponseResult*/@GetMapping("/captchaImage")public ResponseResult getCode(HttpServletResponse response){SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);//生成验证码,及验证码唯一标识String uuid = UUID.randomUUID().toString().replaceAll("-", "");String key = Constants.CAPTCHA_CODE_KEY + uuid;String code = specCaptcha.text().toLowerCase();//保存到redisredisCache.setCacheObject(key,code,1000, TimeUnit.SECONDS);//创建mapHashMap<String,Object> map = new HashMap<>();map.put("uuid",uuid);map.put("img",specCaptcha.toBase64());return new ResponseResult(200,"验证码获取成功",map);}}

2. 创建用户登录对象

 /*** 用户登录对象* @author spikeCong* @date 2023/4/30**/public class LoginBody {/*** 用户名*/private String userName;/*** 用户密码*/private String password;/*** 验证码*/private String code;/*** 唯一标识*/private String uuid = "";public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getUuid() {return uuid;}public void setUuid(String uuid) {this.uuid = uuid;}}

3. LoginController 中创建处理验证码的登录方法

 /*** 登录方法** @param loginBody 登录信息* @return 结果*/@PostMapping("/user/login")public ResponseResult login(@RequestBody LoginBody loginBody){// 生成令牌String token = loginService.login(loginBody.getUserName(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());Map<String,Object> map = new HashMap<>();map.put("token",token);return new ResponseResult(200,"登录成功",map);}

4. LoginService中创建处理验证码的登录方法

 public interface LoginService {String login(String username, String password, String code, String uuid);}
 @Servicepublic class LoginServiceImpl implements LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;/*** 带验证码登录* @param username* @param password* @param code* @param uuid* @return: java.lang.String*/@Overridepublic String login(String username, String password, String code, String uuid) {//从redis中获取验证码String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;String captcha = redisCache.getCacheObject(verifyKey);redisCache.deleteObject(captcha);if (captcha == null || !code.equalsIgnoreCase(captcha)){throw new CaptchaNotMatchException("验证码错误!");}// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameAuthentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));//3.如果认证通过,使用userId生成一个JWT,并将其保存到 ResponseResult对象中返回//3.1 获取经过身份验证的用户的主体信息LoginUser loginUser = (LoginUser) authentication.getPrincipal();//3.2 获取到userID 生成JWTString userId = loginUser.getSysUser().getUserId().toString();String jwt = JwtUtil.createJWT(userId);//4.将用户信息存储在Redis中,在下一次请求时能够识别出用户,userid作为keyredisCache.setCacheObject("login:"+userId,loginUser);//5.封装ResponseResult,并返回return jwt;}}

5.添加自定义异常

 public class CaptchaNotMatchException extends AuthenticationException {public CaptchaNotMatchException(String msg) {super(msg);}public CaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}}

6.配置类中添加配置

 // 对于登录接口 允许匿名访问.mvcMatchers("/user/login","/captchaImage").anonymous()

通常 mvcMatcher 比 antMatcher 更安全:

antMatchers(“/secured”) 仅仅匹配 /secured

mvcMatchers(“/secured”) 匹配 /secured 之余还匹配 /secured/, /secured.html, /secured.xyz

因此 mvcMatcher 更加通用且容错性更高。

7.前后端联调测试

  1. VSCode导入前端项目, 导入带有验证码 security_demo_captcha项目

image.png

注意 node_modules我已经给大家下载好了, 就不需要执行 npm install

  1. npm run serve 启动项目,即可看到生成的验证码

image.png

请求信息

image.png

输入正确的用户名密码,验证码 登录成功.

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_337446.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

六、初始化和清理(1)

本章概要 利用构造器保证初始化方法重载 区分重载方法重载与基本类型返回值的重载无参构造器 this 关键字在构造器中调用构造器static 的含义 利用构造器保证初始化 "不安全"的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题&#xff1a;初始化和清理。…

忽略nan值,沿指定轴计算标准(偏)差numpy.nanstd()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 沿指定轴方向 计算标准(偏)差 numpy.nanstd() [太阳]选择题 import numpy as np a np.array([[1,2],[np.nan,3]]) print("【显示】a ") print(a) print("【执行】np.std(a)&qu…

BUU [网鼎杯 2020 朱雀组]phpweb

BUU [网鼎杯 2020 朱雀组]phpweb 众生皆懒狗。打开题目&#xff0c;只有一个报错&#xff0c;不知何从下手。 翻译一下报错&#xff0c;data()函数:,还是没有头绪&#xff0c;中国有句古话说的好“遇事不决抓个包” 抓个包果然有东西&#xff0c;仔细一看这不就分别是函数和参…

Kaggle图表内容识别大赛TOP方案汇总

赛题名称&#xff1a;Benetech - Making Graphs Accessible 赛题链接&#xff1a;https://www.kaggle.com/competitions/benetech-making-graphs-accessible 赛题背景 数以百万计的学生有学习、身体或视力障碍&#xff0c;导致人们无法阅读传统印刷品。这些学生无法访问科学…

基于RK3588+AI的边缘计算算法方案:智慧园区、智慧社区、智慧物流

RK3588 AI 边缘计算主板规格书简介 关于本文档 本文档详细介绍了基于Rockchip RK3588芯片的AI边缘计算主板外形、尺寸、技术规格&#xff0c;以及详细的硬件接口设计参考说明&#xff0c;使客户可以快速将RK3588边缘计算主板应用于工业互联网、智慧城市、智慧安防、智慧交通&am…

Linux之Shell 编程详解(一)

第 1 章 Shell 概述 1&#xff09;Linux 提供的 Shell 解析器有 [atguiguhadoop101 ~]$ cat /etc/shells /bin/sh /bin/bash /usr/bin/sh /usr/bin/bash /bin/tcsh /bin/csh2&#xff09;bash 和 sh 的关系 [atguiguhadoop101 bin]$ ll | grep bash -rwxr-xr-x. 1 root root …

基于双层优化的大型电动汽车时空调度(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

AI工程师的崛起:填补AI革命中的空白

在一个拥有大约5000名语言学习模型&#xff08;LLM&#xff09;研究员&#xff0c;但大约有5000万软件工程师的世界中&#xff0c;供应限制决定了一种新型专业人才—AI工程师的迅猛增长。他们的崛起不仅仅是一种预测&#xff0c;更是对科技世界动态变化的必然反应。AI工程师作为…

深度学习技巧应用24-深度学习手撕代码与训练流程的联系记忆方法

大家好,我是微学AI,今天给大家介绍一下深度学习技巧应用24-深度学习手撕代码与训练流程的联系记忆方法,大家都知道深度学习模型训练过程是个复杂的过程,这个过程包括数据的收集,数据的处理,模型的搭建,优化器的选择,损失函数的选择,模型训练,模型评估等步骤,其中缺少…

EIP-2535 Diamond standard 实用工具分享

前段时间工作对接到了这标准的协议&#xff0c;于是简单介绍下这个标准分享下方便前端er使用的调用工具 一、标准的诞生 在写复杂逻辑的solidity智能合约时&#xff0c;经常会碰到两个问题&#xff0c;升级和合约大小限制。 升级目前有几种proxy模式&#xff0c;通过delegateca…

【数据结构】【王道408】——PPT截图与思维导图

自用视频PPT截图 视频网址王道B站链接 23考研 408新增考点&#xff1a; 并查集&#xff0c;红黑树 2023年408真题数据结构篇 408考纲解读 考纲变化 目录 第一章 绪论第二章 线性表顺序表单链表双链表循环链表静态链表差别 第三章 栈 队列 数组栈队列栈的应用数组 第四章 串第五…

容器化安装环境EFK搭建

容器化安装环境 Docker中安装并启动ElasticSearch 前置配置 第一步&#xff1a;在宿主机上执行echo “net.ipv4.ip_forward1” >>/usr/lib/sysctl.d/00-system.conf 2.第二步&#xff1a;重启network和docker服务 [rootlocalhost /]# systemctl restart network &&…

DHCP中继代理原理(第二十八课)

当客户机和DHCP服务器不在一个广播域时,DHCP服务器无法接收到客户机的DHCP discover广播数据包,客户机就无法获得IP地址 第一步配置DHCP服务器的信息 <Huawei>u t m //清除日志 Info: Current terminal monitor is off. <Huawei>sys [Huawei]sysname DHCP-R…

数据结构: 线性表(顺序表实现)

文章目录 1. 线性表的定义2. 线性表的顺序表示:顺序表2.1 概念及结构2.2 接口实现2.2.1 顺序表初始化 (SeqListInit)2.2.2 顺序表尾插 (SeqListPushBack)2.2.3 顺序表打印 (SeqListPrint)2.2.6 顺序表销毁 (SeqListDestroy)2.2.5 顺序表尾删 (SeqListPopBack)2.2.6 顺序表头插 …

Python 进阶(二):操作字符串的常用方法

❤️ 博客主页&#xff1a;水滴技术 &#x1f338; 订阅专栏&#xff1a;Python 入门核心技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; 文章目录 一、索引和切片二、字符串长度三、查找和替换四、大小写转换五、分割和连接六、去除空…

《JavaSE-第二十章》之线程 的创建与Thread类

文章目录 什么是进程&#xff1f;什么是线程&#xff1f;为什么需要线程&#xff1f; 基本的线程机制创建线程1.实现 Runnable 接口2.继承 Thread 类3.其他变形 Thread常见构造方法1. Thread()2. Thread(Runnable target)3. Thread(String name)4. Thread(Runnable target, Str…

C语言每天一练----输出水仙花数

题目&#xff1a;请输出所有的"水仙花数" 题解&#xff1a;所谓"水仙花数"是指一个3位数,其各位数字立方和等于该数本身。 例如, 153是水仙花数, 因为153 1 * 1 * 1 5 * 5 * 5 3 * 3 * 3" #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h&g…

Segmentation fault 利用 core.xxx文件帮助你debug

在没有get到本文介绍的技能之前的时候&#xff0c;以前遇到程序发生了 Segmentation fault 时&#xff0c;也是一筹莫展&#xff0c;看到伴随程序崩溃而生成的 core.xxxx 文件时&#xff08;有时会生成&#xff0c;有时不会生成&#xff0c;留着下面介绍&#xff09;&#xff0…

windows系统之WSL 安装 Ubuntu

WSL windows10 以上才有这个wsl功能 WSL&#xff1a; windows Subsystem for Linux 是应用于Windows系统之上的Linux子系统 作用很简单&#xff0c;可以在Windows系统中获取Linux系统环境&#xff0c;并完全直连计算机硬件&#xff0c;无需要通过虚拟机虚拟硬件 Windows10的W…

TCP网络通信编程之字符流

【案例1】 【题目描述】 【 注意事项】 (3条消息) 节点流和处理流 字符处理流BufferedReader、BufferedWriter&#xff0c;字节处理流-BufferedInputStream和BufferedOutputStream (代码均正确且可运行_Studying~的博客-CSDN博客 1。这里需要使用字符处理流&#xff0c;来将…