当前位置: 首页 > news >正文

SpringSecurity6(认证-前后端分离)

文章目录

    • 1.环境
    • 2.pom
    • 3.application.yml
    • 4.SpringSecurity6主配置类
    • 5.其他配置类
      • 5.1.MyUserDetailService
      • 5.2.MyAuthenticationSuccessHandler
      • 5.3.MyAuthenticationFailureHandler
      • 5.4.VerifyCodeAuthenticationException
      • 5.5.AuthCodeFilter
      • 5.6.User
      • 5.7.MyUser
    • 6.测试

1.环境

SpringBoot3.2.8、JDK17、SpringSecurity6.1.11、Redis6.0.8

认证涉及:

  1. 验证码过滤器及用户名密码认证
  2. 密码加密及密码自动升级更新
  3. 记住我
  4. 并发会话
  5. 会话存储
  6. csrf防护
  7. cros跨域(未配置,有兴趣自行实现)
  8. 统一异常处理

2.pom

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.8</version>
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-thymeleaf</artifactId>-->
<!--        </dependency>--><!--springboot3.0接入mybatisPlus的依赖--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.51</version></dependency><!--redis会话存储相关--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId><version>3.2.4</version></dependency></dependencies>

3.application.yml

server:port: 8080servlet:session:timeout: 30spring:security:user:name: rootpassword: 123456thymeleaf:cache: falseprefix: classpath:/templates/suffix: .htmldatasource:url: jdbc:mysql://192.168.159.100:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverhikari:# 最大连接池数量maximum-pool-size: 20# 最小空闲线程数量minimum-idle: 10# 配置获取连接等待超时的时间connectionTimeout: 30000# 校验超时时间validationTimeout: 5000# 空闲连接存活最大时间,默认10分钟idleTimeout: 600000# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟maxLifetime: 1800000# 连接测试query(配置检测连接是否有效)connectionTestQuery: SELECT 1type: com.zaxxer.hikari.HikariDataSourcedata:redis:port: 6379host: 192.168.159.100
logging:level:org:springframework:security: TRACEmybatis-plus:mapper-locations: classpath:com.linging.mapper/*Mapper.xml

4.SpringSecurity6主配置类

@Configuration
@EnableWebSecurity // 高版本使用注解
@EnableRedisIndexedHttpSession  // 注入FindByIndexNameSessionRepository,实现redis持久化会话
public class SecurityConfig {@Resourceprivate MyUserDetailService myUserDetailService;@Resourceprivate DataSource dataSource;@Resourceprivate FindByIndexNameSessionRepository<? extends Session> sessionRepository;public PasswordEncoder passwordEncoder(){// 该类的升级算法由springSecurity指定,无法直接修改,不过可以复制出来,自行修改//PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();String encodingId = "MD5"; //指定升级的加密算法Map<String, PasswordEncoder> encoders = new HashMap();encoders.put("bcrypt", new BCryptPasswordEncoder());encoders.put("ldap", new LdapShaPasswordEncoder());encoders.put("MD4", new Md4PasswordEncoder());encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new StandardPasswordEncoder());encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());// SpringSecurity默认的加密代理就是DelegatingPasswordEncoderreturn new DelegatingPasswordEncoder(encodingId, encoders);}/*** 全局配置AuthenticationManager* 定义AuthenticationManager,加入两种AuthenticationProvider*/@Beanpublic AuthenticationManager authenticationManager() {// 保留原来账号密码登录的AuthenticationProviderDaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(myUserDetailService); // 指定用户认证的服务daoAuthenticationProvider.setUserDetailsPasswordService(myUserDetailService); // 指定加密密码自动更新的服务//daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher());return providerManager;}// 这种加密方式,只能指定唯一的加密算法,无法自动升级@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder(){BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();return passwordEncoder;}// token持久化@Beanpublic PersistentTokenRepository myPersistentTokenRepository(){JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();repository.setDataSource(dataSource);//repository.setCreateTableOnStartup(true); //启动创建表,mysql创建语句有问题,可以重写或者手动创建/**create table persistent_logins (username varchar(64) not null,series varchar(64) primary key,token varchar(64) not null,last_used timestamp NOT null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)*/return repository;}// 记住我持久化实现逻辑@Beanpublic RememberMeServices rememberMeServices(){// myUserDetailService 记住我自动登录逻辑// new InMemoryTokenRepositoryImpl() 记住我持久化实现,内存return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),myUserDetailService,   // 指定记住我时的认证的调用服务myPersistentTokenRepository()); // 记住我token持久化的实现}// 验证码+用户密码认证 过滤器@Beanpublic AuthCodeFilter authCodeFilter(){AuthCodeFilter authCodeFilter = new AuthCodeFilter();authCodeFilter.setFilterProcessesUrl("/doLogin");authCodeFilter.setAuthenticationManager(authenticationManager());authCodeFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());authCodeFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());// 记住我,需配置两个地方authCodeFilter.setRememberMeServices(rememberMeServices());// springSecurity6需要手动设置,不然默认使用RequestAttributeSecurityContextRepositoryauthCodeFilter.setSecurityContextRepository(securityContextRepository());// 会话策略authCodeFilter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy());return authCodeFilter;}// 代理认证上下文,DelegatingSecurityContextRepository,认证后的用户信息存放在哪里@Beanpublic SecurityContextRepository securityContextRepository(){return new DelegatingSecurityContextRepository(new RequestAttributeSecurityContextRepository(),new HttpSessionSecurityContextRepository());}// 返回会话策略代理@Beanpublic CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy(){List<SessionAuthenticationStrategy> strategies = new ArrayList<>();// 顺序:先 并发控制会话 再 注册会话,顺序不可变strategies.add(controlAuthenticationStrategy());strategies.add(changeSessionIdAuthenticationStrategy());strategies.add(registerSessionAuthenticationStrategy());// 多个会话策略构成,按顺序执行return new CompositeSessionAuthenticationStrategy(strategies);}// 并发会话策略@Beanpublic ConcurrentSessionControlAuthenticationStrategy controlAuthenticationStrategy(){ConcurrentSessionControlAuthenticationStrategy authenticationStrategy =new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());authenticationStrategy.setMaximumSessions(1);// ExceptionIfMaximumExceeded 默认false,当超过最大session数时// true: 不允许新session, 保持旧session// false: 销毁旧session, 新session生效authenticationStrategy.setExceptionIfMaximumExceeded(false);return authenticationStrategy;}// 会话注册策略@Beanpublic RegisterSessionAuthenticationStrategy registerSessionAuthenticationStrategy(){return new RegisterSessionAuthenticationStrategy(sessionRegistry());}// 默认会话策略@Beanpublic ChangeSessionIdAuthenticationStrategy changeSessionIdAuthenticationStrategy(){return new ChangeSessionIdAuthenticationStrategy();}// 在线会话存储@Beanpublic SessionRegistry sessionRegistry(){// new SessionRegistryImpl(); 实现为内存,使用ConcurrentHashMap维护// 使用redis来维护sessionreturn new SpringSessionBackedSessionRegistry(sessionRepository);}// 会话事件发布,监听会话的创建、销毁、并更,发布对应事件@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}// 并发会话过滤器@Beanpublic ConcurrentSessionFilter concurrentSessionFilter(){return new ConcurrentSessionFilter(sessionRegistry(), strategy -> {HttpServletResponse response = strategy.getResponse();response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);response.getWriter().write("已在另一个地方登录");});}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authz -> authz//.requestMatchers("/index/**", "/login.html", "/error.html").permitAll()  // 放行的路径,不认证就可以访问.anyRequest().authenticated() // 剩余的其他路径都要认证)
//                .formLogin(configurer -> configurer
//                        .loginProcessingUrl("/doLogin")  //登录接口
//                        .successHandler(new MyAuthenticationSuccessHandler()) // 登录成功,返回json
//                        .failureHandler(new MyAuthenticationFailureHandler()) //登录失败,返回json
//                ).addFilterAt(authCodeFilter(), UsernamePasswordAuthenticationFilter.class) // 替换过滤器.addFilterAt(concurrentSessionFilter(), ConcurrentSessionFilter.class).exceptionHandling(ex -> {ex.authenticationEntryPoint((request, response, authException) -> {// 当未认证的用户尝试访问受保护资源时的处理逻辑response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("未认证");}).accessDeniedHandler((request, response, authException) -> {//当已认证的用户尝试访问其无权限访问的资源时,会触发访问拒绝处理器response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write("权限不足");});})//.authenticationManager(authenticationManager)  //本地配置//.userDetailsService(myUserDetailService)  //可以不注入,只要容器中有实例,security会自动注入.rememberMe(cus -> { //记住我,需配置两个地方cus.rememberMeServices(rememberMeServices());}).securityContext(cus -> {cus.securityContextRepository(securityContextRepository()).requireExplicitSave(true);}).httpBasic(withDefaults())
//                .csrf(cus -> {  // 开启csrf跨站脚本攻击防护
//                    cus.csrfTokenRepository(new CookieCsrfTokenRepository()) // csrftoken保存在cookie中
//                       .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()); // csrfToken设置请求处理器,默认为XorCsrfTokenRequestAttributeHandler,此为加密token,需要额外处理传递的csrftoken
//                });.csrf(AbstractHttpConfigurer::disable); //测试暂时关闭return http.build();}
}

5.其他配置类

5.1.MyUserDetailService

/*** 自定义用户认证介质UserDetailsService* 自定义用户密码自动更新介质UserDetailsPasswordService*/
@Component
public class MyUserDetailService implements UserDetailsService, UserDetailsPasswordService {@Resourceprivate UserService userService;@Resourceprivate RoleService roleService;/*** 加载用户名称* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userService.getOne(new QueryWrapper<User>().eq("username", username), false);if(user == null){throw new UsernameNotFoundException("用户名不正确");}MyUser myUser = new MyUser();BeanUtils.copyProperties(user, myUser);List<Role> roleList = roleService.getByUserId(user.getId());myUser.setRoles(roleList);return myUser;}/*** 当加密策略变更时,会更新数据库的密码为新密码* @param user* @param newPassword* @return*/@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {String username = user.getUsername();boolean update = userService.update(new UpdateWrapper<User>().set("password", newPassword).eq("username", username));if(update){MyUser myUser = (MyUser) user;myUser.setPassword(newPassword);}return user;}
}

5.2.MyAuthenticationSuccessHandler

/*** 自定义认证成功json返回*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<Object, Object> map = new HashMap<>();map.put("msg", "登录成功");map.put("code", 200);map.put("authentication", authentication);response.setContentType("application/json;charset=utf-8");response.getWriter().write(new ObjectMapper().writeValueAsString(map));}
}

5.3.MyAuthenticationFailureHandler

/*** 自定义认证失败json返回*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {Map<Object, Object> map = new HashMap<>();map.put("msg", "出现错误");map.put("code", 500);map.put("exception", exception.getMessage());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(new ObjectMapper().writeValueAsString(map));}
}

5.4.VerifyCodeAuthenticationException

/*** 验证码异常,集成AuthenticationException,否则无法被框架捕获*/
public class VerifyCodeAuthenticationException extends AuthenticationException {public VerifyCodeAuthenticationException(String msg, Throwable cause) {super(msg, cause);}public VerifyCodeAuthenticationException(String msg) {super(msg);}
}

5.5.AuthCodeFilter

/*** 验证码+用户名和密码过滤器*/
@Component
public class AuthCodeFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String authCode = request.getParameter("authCode");if(authCode == null){throw new VerifyCodeAuthenticationException("验证码不能为空");}if(!authCode.equalsIgnoreCase(getAuthCode(obtainUsername(request)))){throw new VerifyCodeAuthenticationException("验证码错误");}return super.attemptAuthentication(request,response);}private String getAuthCode(String username) {return "123456";}
}

5.6.User

@Data
public class User implements Serializable {private static final long serialVersionUID = 1L;/*** id*/@TableId(value = "id", type = IdType.AUTO)protected Integer id;/*** 账号*/protected String username;/*** 密码*/protected String password;/*** 是否启用*/protected boolean enabled;/*** 账号是否过期*/protected boolean accountNonExpired;/*** 账号是否锁定*/protected boolean accountNonLocked;/*** 密码是否过期*/protected boolean credentialsNonExpired;
}

5.7.MyUser

@Data
public class MyUser extends User implements UserDetails {private List<Role> roles = new ArrayList<>();@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return roles.stream().map(role ->new SimpleGrantedAuthority(role.getName())).collect(Collectors.toSet());}@Overridepublic boolean isAccountNonExpired() {return super.isAccountNonExpired();}@Overridepublic boolean isAccountNonLocked() {return super.isAccountNonLocked();}@Overridepublic boolean isCredentialsNonExpired() {return super.isCredentialsNonExpired();}@Overridepublic boolean isEnabled() {return super.isEnabled();}@Overridepublic boolean equals(Object o) {if (o == null || getClass() != o.getClass()) return false;User user = (User) o;return Objects.equals(super.username, user.getUsername());}@Overridepublic int hashCode() {return Objects.hashCode(super.username);}
}

6.测试

登录接口,此处登录参数使用表单的方式,如要使用json传递参数,可对上面AuthCodeFilter extends UsernamePasswordAuthenticationFilter中将UsernamePasswordAuthenticationFilter中获取参数的方式进行重写,也是比较简单的,可自行实现。
在这里插入图片描述
持久化token表结构:
在这里插入图片描述
user表结构:
在这里插入图片描述
role表结构:
在这里插入图片描述
user_role表结构:
在这里插入图片描述

相关文章:

  • MATLAB GUI界面设计 第四章——图像的绘制与显示
  • 电路图识图基础知识-塔式起重机控制电路识图与操作要点(三十五)
  • 深入解析 Windows 文件查找命令(dir、gci)
  • 窗口函数的概念
  • 为什么android要使用Binder机制
  • 顶级思维方式——认知篇九(经典语录)《约翰·克利斯朵夫》
  • LangChain4j从入门到实战(一)
  • DeepSeek今天喝什么随机奶茶推荐器
  • [C#] Task
  • 飞算 JavaAI:重构 Java 开发范式的工程化实践
  • Prim(普里姆)算法
  • 网络/信号/电位跟踪
  • 嘉讯科技:医院电子病历系统的关键性作用
  • 60天python训练计划----day56
  • 深入浅出Node.js后端开发
  • UE5 AnimMontage 的混合(Blend)模式
  • Qt for OpenHarmony 编译鸿蒙调用的动态库
  • WPF中MVVM和MVVMLight模式
  • 卷积运算的历史
  • Gateway路径匹配规则易错点
  • python怎么做网站/如何利用互联网进行宣传推广
  • 哪些网站可以在线做动图/百度推广客服人工电话多少
  • 做地方网站论坛/品牌推广策略有哪些
  • 做系统正版win10系统下载网站/小红书软文推广
  • 东莞网站建设套餐/武汉企业网站推广
  • wordpress视频广告插件/老铁seo外链工具