🎉🎉欢迎来到我的CSDN主页!🎉🎉
🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚
🌟推荐给大家我的专栏《Spring Security》。🎯🎯
👉点击这里,就可以查看我的主页啦!👇👇
Java方文山的个人主页
🎁如果感觉还不错的话请给我点赞吧!🎁🎁
💖期待你的加入,一起学习,一起进步!💖💖
我们都知道Spring Security是做认证的,那它到底是怎么认证的呢?它是怎么将明文密码加密的呢?Token令牌的使用与CSRF跨域请求伪造是什么等等我们都不知道,但是通过这篇文章我相信你会有所了解有所收获!!!
基于Spring Initializr创建SpringBoot项目,实现与MyBatisPlus的项目整合。分别导入:CodeGenerator和MyBatisPlusConfig。
CodeGenerator:用于MybatisPlus代码生成; MyBatisPlusConfig:MyBatisPlus配置类,实现了分页和乐观锁相关配置。
org.springframework.boot spring-boot-starter-freemarkerorg.springframework.boot spring-boot-starter-webmysql mysql-connector-javaruntime 5.1.44 org.projectlombok lomboktrue org.springframework.boot spring-boot-starter-testtest com.baomidou mybatis-plus-boot-starter3.5.2 com.baomidou mybatis-plus-generator3.5.2 com.alibaba fastjson2.0.32
spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.jdbc.Driver username: root password: 1234 url: jdbc:mysql://localhost:3306/vue?useUnicode=true&characterEncoding=utf8&useSSL=false freemarker: enabled: true suffix: .ftl template-loader-path: classpath:/templates/ mybatis-plus: # Mybatis Mapper所对应的XML位置 mapper-locations: classpath:mapper/*.xml # 别名包扫描路径 type-aliases-package: com.zking.spbootauthc.model # 是否开启自动驼峰命名规则(camel case)映射 configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) logging: level: com.zking.spbootauthc.mapper: debug
修改数据库相关账号、密码及数据库名。
表名 | 说明 |
---|---|
sys_user | 用户信息表 |
sys_role | 角色信息表 |
sys_module | 模块信息表(权限信息表) |
sys_user_role | 用户角色表 |
sys_role_module | 角色模块表 |
表之间的关系说明:
数据表中有以上字段即可,后续我们还要加的。
直接运行CodeGenerator.java类,生成sys_开头的相关信息表。
package com.csdn.security.config; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.Collections; import java.util.List; @Slf4j public class MySQLGenerator { private final static String URL = "jdbc:mysql://localhost:3306/goods?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC"; private final static String USERNAME = "root"; private final static String PASSWORD = "root789"; private final static DataSourceConfig.Builder DATA_SOURCE_CONFIG = new DataSourceConfig.Builder(URL, USERNAME, PASSWORD); public static void main(String[] args) { FastAutoGenerator.create(DATA_SOURCE_CONFIG) .globalConfig( (scanner, builder) -> builder.author("Java方文山") .outputDir(System.getProperty("user.dir") + "\\src\\main\\java") .commentDate("yyyy-MM-dd") .dateType(DateType.TIME_PACK) ) .packageConfig((builder) -> builder.parent("com.csdn.boot") .entity("pojo") .service("Service") .serviceImpl("Service.impl") .mapper("mapper") .xml("mapper.xml") .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "\\src\\main\\resources\\mapper")) ) .injectionConfig((builder) -> builder.beforeOutputFile( (a, b) -> log.warn("tableInfo: " + a.getEntityName()) ) ) .strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all"))) .addTablePrefix("tb_", "t_", "lay_", "meeting_", "sys_") .entityBuilder() .enableChainModel() .enableLombok() .enableTableFieldAnnotation() .controllerBuilder() .enableRestStyle() .enableHyphenStyle() .build() ) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } protected static ListgetTables(String tables) { return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(",")); } }
前面说到有些字段需要后期加,至于是什么字段我事先没有给大家说的原因是想大家知道,为什么要加这些字段,这些字段是什么意思,首先在SecurityFilterChain过滤链编写重定向登录成功的首页方法
//设置登录成功后重定向到那个页面 .successHandler((req,resp,auth)->{ resp.sendRedirect("/index"); })
方便我们查看里面到底有什么参数
可以看到以上除了username\password都是我们没有的,我们接下来就把这些属性加上即可,因为这里Security必须要用到UserDetails这个类,所以我们自己的user类就就需要实现这个类。
UserDetails是Spring Security框架中的一个接口,它代表了应用程序中的用户信息。UserDetails接口定义了一组方法,用于获取用户的用户名、密码、角色和权限等信息,以便Spring Security可以使用这些信息进行身份验证和授权。
以下是UserDetails接口中定义的方法:
getUsername():获取用户的用户名。
getPassword():获取用户的密码。
getAuthorities():获取用户的角色和权限信息。
isEnabled():判断用户是否可用。
isAccountNonExpired():判断用户的账号是否过期。
isAccountNonLocked():判断用户的账号是否被锁定。
isCredentialsNonExpired():判断用户的凭证是否过期。
自定义用户信息时,可以实现UserDetails接口并覆盖其中的方法来提供自己的用户信息。
我们直接定义这些属性即可,随后在数据库加上这些属性,注意默认值为1,否则认证不成功,因为除了0之外的数字都是真,如果为0就是假也就是false,那么我们认证的字段有一个为false必然是不成功的。
修改UserServiceImpl并实现UserDetailsService,重写loadUserByUsername(String username)方法。
@Service public class UserServiceImpl extends ServiceImplimplements IUserService, UserDetailsService { /** * 实现Spring Security内置的UserDetailService接口,重写loadUserByUsername方法实现数据库的身份校验 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询数据库中用户信息 User user = this.getOne(new QueryWrapper ().eq("username", username)); //判断用户是否存在 if(Objects.isNull(user)) throw new UsernameNotFoundException("用户不存在"); //权限校验TODO,后续讲解 return user; } }
UserDetailsService是Spring Security中的一个接口,它用于从特定数据源(如数据库)中获取用户详细信息,以进行身份验证和授权。实现该接口的类需要实现loadUserByUsername方法,该方法根据给定的用户名返回一个UserDetails对象,该对象包含有关用户的详细信息,例如密码、角色和权限等。在Spring Security中,UserDetailsService通常与DaoAuthenticationProvider一起使用,后者是一个身份验证提供程序,用于验证用户的凭据。
创建WebSecurityConfig配置类,配置SpringSecurity结合数据库方式进行身份认证和权限鉴定。
package com.csdn.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration //开启SpringSecurity的默认行为 @EnableWebSecurity public class WebSecurityConfig { /** * 配置密码编码器,首次采用明文密码方式进行比对校验 */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 基于数据库方式进行身份认证和权限鉴定 */ @Autowired private UserDetailsService userDetailsService; /** * 获取AuthenticationManager(认证管理器),登录时认证使用(基于数据库方式) * @param * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager() throws Exception { //创建DaoAuthenticationProvider DaoAuthenticationProvider provider=new DaoAuthenticationProvider(); //设置userDetailsService,基于数据库方式进行身份认证 provider.setUserDetailsService(userDetailsService); //配置密码编码器 provider.setPasswordEncoder(passwordEncoder()); return new ProviderManager(provider); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() // 开放接口访问权限,不需要登录就可以访问 .antMatchers("/toLogin").permitAll() // 设置角色权限 .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") // 其余所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .formLogin() // 设置登录页面的 URL .loginPage("/toLogin") // 设置登录请求的 URL,即表单提交的 URL .loginProcessingUrl("/userLogin") // 设置登录表单中用户名字段的参数名,默认为username .usernameParameter("username") // 设置登录表单中密码字段的参数名,默认为password .passwordParameter("password") //设置登录成功后重定向到那个页面 .successHandler((req,resp,auth)->{ resp.sendRedirect("/index"); }) .and() .logout() // 设置安全退出的URL路径 .logoutUrl("/logout") // 设置退出成功后跳转的路径 .logoutSuccessUrl("/") ; //跳转登录页重新登录问题:用于禁用 CSRF(Cross-Site Request Forgery)防护机制。 http.csrf().disable(); //定义访问被拒绝时的处理方式,会重定向到noauth(也就是没有权限的页面展示) http.exceptionHandling().accessDeniedPage("/noauth"); return http.build(); } }
至此就可以完成了通过数据库的用户进行认证登录操作了,这里需要注意的是formLogin认证失败后将不在使用failureForwardUrl()方法转发,而是使用failureHandler处理器方式处理错误信息并跳转页面。
如果使用failureHandler处理器方式,则可以自定义错误页面及错误信息:
.failureHandler((request, response, exception) -> { //将认证错误信息保存到request作用域,取名为msg request.setAttribute("msg",exception.getMessage()); //认证失败后转发到指定页面 request.getRequestDispatcher("/").forward(request,response); })
修改登录页面login.ftl加入错误信息展示区域:
${msg!}
Spring Security提供了多种密码加密方式,大致可以归类于以下几种:
对密码进行明文处理,即不采用任何加密方式;
采用MD5加密方式;
采用哈希算法加密方式;
创建自定义MD5加密类并实现PasswordEncoder:
public class CustomMd5PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { //对密码进行 md5 加密 String md5Password = DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()); System.out.println(md5Password); return md5Password; } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 通过md5校验 System.out.println(rawPassword); System.out.println(encodedPassword); return encode(rawPassword).equals(encodedPassword); } }
修改SecurityConfig配置类,更换密码编码器:
@Bean public PasswordEncoder passwordEncoder(){ // 自定义MD5加密方式: return new CustomMd5PasswordEncoder(); }
数据库中的用户密码也需要更换成对应自定义MD5加密密码:
//MD5自定义加密方式: String pwd = DigestUtils.md5DigestAsHex("123456".getBytes()); System.out.println(pwd);
最后,将生成的MD5加密密码保存到数据库表中。
BCryptPasswordEncoder是Spring Security中一种基于bcrypt算法的密码加密方式。bcrypt算法是一种密码哈希函数,具有防止彩虹表攻击的优点,因此安全性较高。
使用BCryptPasswordEncoder进行密码加密时,可以指定一个随机生成的salt值,将其与原始密码一起进行哈希计算。salt值可以增加密码的安全性,因为即使两个用户使用相同的密码,由于使用不同的salt值进行哈希计算,得到的哈希值也是不同的。
在Spring Security中,可以通过在SecurityConfig配置类中添加以下代码来使用BCryptPasswordEncoder进行密码加密:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
这样就可以在Spring Security中使用BCryptPasswordEncoder进行密码加密了,细心的同志在刚刚认证的时候应该就发现了。
BCrypt是一种单向哈希函数,它使用salt(盐)和cost(成本因子)来增加哈希的安全性。
下面是BCryptPasswordEncoder的工作原理:
加密过程:
这里假如有两个密码都为123但是salt不一样,可能是18293也可能是18392,但是他们的密码都是123,只是将原本的密码加上了“盐”后再进行哈希算法得到了一个值。
验证过程:
在实际开发中,为了用户登录方便常常会启用记住我(Remember-Me)功能。如果用户登录时勾选了“记住我”选项,那么在一段有效时间内,会默认自动登录,免去再次输入用户名、密码等登录操作。该功能的实现机理是根据用户登录信息生成 Token 并保存在用户浏览器的 Cookie 中,当用户需要再次登录时,自动实现校验并建立登录态的一种机制。
Spring Security提供了两种 Remember-Me 的实现方式:
简单加密 Token:用散列算法加密用户必要的登录系信息并生成 Token 令牌。
持久化 Token:数据库等持久性数据存储机制用的持久化 Token 令牌。
基于持久化Token配置步骤如下:
创建数据库表 persistent_logins,用于存储自动登录信息
CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) PRIMARY KEY, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL );
该步骤可以不做,在后续的配置过程中可以交由SpringSecurity自动生成。
基于持久化Token配置,修改SecurityConfig配置类:
Remember-Me 功能的开启需要在configure(HttpSecurity http)方法中通过http.rememberMe()配置,该配置主要会在过滤器链中添加 RememberMeAuthenticationFilter 过滤器,通过该过滤器实现自动登录。
@Resource public DataSource dataSource; /** * 配置持久化Token方式,注意tokenRepository.setCreateTableOnStartup()配置 */ @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 设置为true要保障数据库该表不存在,不然会报异常哦 // 所以第二次打开服务器应用程序的时候得把它设为false tokenRepository.setCreateTableOnStartup(false); return tokenRepository; }
在SecurityFilterChain中添加规则
.and() .rememberMe() // 指定 rememberMe 的参数名,用于在表单中携带 rememberMe 的值。 //.rememberMeParameter("remember-me") // 指定 rememberMe 的有效期,单位为秒,默认2周。 .tokenValiditySeconds(30) // 指定 rememberMe 的 cookie 名称。 .rememberMeCookieName("remember-me-cookie") // 指定 rememberMe 的 token 存储方式,可以使用默认的 PersistentTokenRepository 或自定义的实现。 .tokenRepository(persistentTokenRepository()) // 指定 rememberMe 的认证方式,需要实现 UserDetailsService 接口,并在其中查询用户信息。 .userDetailsService(userDetailsService)
rememberMe主要方法介绍:
方法 | 说明 |
---|---|
rememberMeParameter() | 指定在登录时“记住我”的 HTTP 参数,默认为 remember-me |
tokenValiditySeconds() | 设置 Token 有效期为 200s,默认时长为 2 星期 |
tokenRepository() | 指定 rememberMe 的 token 存储方式,可以使用默认的 PersistentTokenRepository 或自定义的实现 |
userDetailsService() | 指定 UserDetailsService 对象 |
rememberMeCookieName() | 指定 rememberMe 的 cookie 名称 |
修改登录页面login.ftl,添加remember-Me记住我的checkbox选项框。
注意:配置的checkbox复选框的name属性名要与上面配置的rememberMeParameter("属性名")一致,默认就叫remember-me。
总结:remember-me 只有在 JSESSIONID 失效和SecurityContextPersistenceFilter 过滤器认证失败或者未进行认证时才发挥作用。此时,只要 remember-me 的 Cookie 不过期,我们就不需要填写登录表单,就能实现再次登录,并且 remember-me 自动登录成功之后,会生成新的 Token 替换旧的 Token,相应 Cookie 的 Max-Age 也会重置。
下图是我们没有加Token令牌如果反复刷新会要我们重新提交表单
加了Token之后并且勾选复选框记住我将Token保存到cookie中
数据表中也会有数据
在Spring Security中,防范CSRF攻击可以通过启用CSRF保护来实现。启用CSRF保护后,Spring Security会自动在每个表单中添加一个隐藏的CSRF Token字段,并在服务器端进行验证。如果Token验证失败,则会抛出异常,从而拒绝执行请求。启用CSRF保护的方式是在Spring Security配置文件中添加.csrf()方法,例如:
http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
将原来的关闭CSRF注释掉换成我们的
在上面的配置中,我们使用了CookieCsrfTokenRepository作为CSRF Token的存储方式,并设置了httpOnly为false,以便在客户端可以访问到该Token。
.csrf()主要方法介绍:
方法 | 说明 |
---|---|
disable() | 关闭CSRF防御 |
csrfTokenRepository() | 设置CookieCsrfTokenRepository实例,用于存储和检索CSRF令牌。与HttpSessionCsrfTokenRepository不同,CookieCsrfTokenRepository将CSRF令牌存储在cookie中,而不是在会话中。 |
ignoringAntMatchers() | 设置一组Ant模式,用于忽略某些请求的CSRF保护。例如,如果您想要忽略所有以/api/开头的请求,可以使用.ignoringAntMatchers("/api/**")。 |
csrfTokenManager() | 设置CsrfTokenManager实例,用于管理CSRF令牌的生成和验证。默认情况下,Spring Security使用DefaultCsrfTokenManager实例来生成和验证CSRF令牌。 |
requireCsrfProtectionMatcher() | 设置RequestMatcher实例,用于确定哪些请求需要进行CSRF保护。默认情况下,Spring Security将对所有非GET、HEAD、OPTIONS和TRACE请求进行CSRF保护。 |
使用了 spring-security 后,默认开启了防止跨域攻击的功能,任何 POST 提交到后台的表单都要验证是否带有 _csrf 参数,一旦传来的 _csrf 参数不正确,服务器便返回 403 错误。
修改login.ftl页面代码,加入_csrf隐藏域。
如果针对一些特定的请求接口,不需要进行CSRF防御,可以通过以下配置忽略:
http.csrf().ignoringAntMatchers("/upload"); // 禁用/upload接口的CSRF防御
到这里我的分享就结束了,欢迎到评论区探讨交流!!
💖如果觉得有用的话还请点个赞吧 💖