1.前期准备
基于Spring Initializr创建SpringBoot项目(基于SpringBoot 2.7.12版本),实现与MyBatisPlus的项目整合。分别导入:CodeGenerator和MyBatisPlusConfig。
CodeGenerator:用于MybatisPlus代码生成;MyBatisPlusConfig:MyBatisPlus配置类,实现了分页和乐观锁相关配置。
1.1.添加pom.xml依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>5.1.44</version> </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>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.32</version> </dependency>
1.2.配置application.yml
server: port: 8080 spring: freemarker: # 设置freemarker模板后缀 suffix: .ftl # 设置freemarker模板前缀 template-loader-path: classpath:/templates/ enabled: true # security: # user: # name: admin # password: 123456 datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/bookshop?useUnicode=true&characterEncoding=utf8&useSSL=false
修改数据库相关账号、密码及数据库名。
1.3.导入相关数据表
将课件资料中的sql/db.sql导入到数据库中。
| 表名 | 说明 |
|---|---|
| sys_user | 用户信息表 |
| sys_role | 角色信息表 |
| sys_module | 模块信息表(权限信息表) |
| sys_user_role | 用户角色表 |
| sys_role_module | 角色模块表 |
表之间的关系说明:
1.4.实现MP代码生成
直接运行CodeGenerator.java类,生成sys_开头的相关信息表。
此处省略一万字...
2.SpringSecurity之认证
2.1.导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.2.创建登录页及首页
基于freemarker模板引擎创建登录页login.ftl和首页index.ftl。
login.ftl首页部分代码如下:
<h1>用户登录</h1>
<form action="/user/userLogin" method="post">
<label>账号:</label><input type="text" name="username"/><br/>
<label>密码:</label><input type="password" name="password"/><br/>
<input type="submit" value="登 录"/>
</form>
index.ftl代码如下:
<h1>首页</h1>
<div style="position: absolute;top:15px;right:15px;"><a href="/logout">安全退出</a></div>
href="/logout"为SpringSecurity配置的退出请求路径。主要完成以下几个操作:
清除指定
Cookie:CookieClearingLogoutHandler#logout清除
remember-me:PersistentTokenBasedRememberMeServices#logout使当前
Session无效,清空当前的SecurityContext中认证用户信息Authentication
2.3.创建配置Controller
创建IndexController配置跳转首页和登录页的接口。
@Controller
public class IndexController {
@RequestMapping("/")
public String toLogin(){
return "login";
}
@RequestMapping("/index")
public String toIndex(){
return "index";
}
}
在UserController中配置请求登录接口:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/userLogin")
public String userLogin(User user){
return "login";
}
}
2.4.用户认证
2.4.1.用户对象UserDetails(UserDetails用来处理前端发送的账号密码请求)
修改User并实现UserDetails。
@Getter
@Setter
@TableName("sys_user")
public class User implements Serializable, UserDetails {
...
/**
* 用户权限集合
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 用户没过期返回true,反之则false
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 用户没锁定返回true,反之则false
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用户凭据(通常为密码)没过期返回true,反之则false
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用户是启用状态返回true,反之则false
*/
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails是Spring Security框架中的一个接口,它代表了应用程序中的用户信息。UserDetails接口定义了一组方法,用于获取用户的用户名、密码、角色和权限等信息,以便Spring Security可以使用这些信息进行身份验证和授权。
以下是UserDetails接口中定义的方法:
-
getUsername():获取用户的用户名。 -
getPassword():获取用户的密码。 -
getAuthorities():获取用户的角色和权限信息。 -
isEnabled():判断用户是否可用。 -
isAccountNonExpired():判断用户的账号是否过期。 -
isAccountNonLocked():判断用户的账号是否被锁定。 -
isCredentialsNonExpired():判断用户的凭证是否过期。
自定义用户信息时,可以实现UserDetails接口并覆盖其中的方法来提供自己的用户信息。
2.4.2.业务对象UserDetailsService
修改UserServiceImpl并实现UserDetailsService,重写loadUserByUsername(String username)方法。
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService,UserDetailsService { /** * 实现Spring Security内置的UserDetailService接口,重写loadUserByUsername方法实现数据库的身份校验 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名查询数据库中用户信息 User user = this.getOne(new QueryWrapper<User>().eq("username", username)); //判断用户是否存在 if(Objects.isNull(user)) throw new UsernameNotFoundException("用户不存在"); //权限校验TODO,后续讲解 return user; } }
UserDetailsService是Spring Security中的一个接口,它用于从特定数据源(如数据库)中获取用户详细信息,以进行身份验证和授权。实现该接口的类需要实现loadUserByUsername方法,该方法根据给定的用户名返回一个UserDetails对象,该对象包含有关用户的详细信息,例如密码、角色和权限等。在Spring Security中,UserDetailsService通常与DaoAuthenticationProvider一起使用,后者是一个身份验证提供程序,用于验证用户的凭据。
2.4.3.SecurityConfig配置
创建WebSecurityConfig配置类,配置SpringSecurity结合数据库方式进行身份认证和权限鉴定。
@Configuration @EnableWebSecurity public class WebSecurityConfig { /** * 基于数据库方式进行身份认证和权限鉴定 */ @Autowired private UserDetailsService userDetailsService; /** * 配置密码编码器,首次采用明文密码方式进行比对校验 */ @Bean public PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } /** * 获取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("/").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/") .loginProcessingUrl("/user/userLogin") .passwordParameter("password") .usernameParameter("username") // 认证成功 redirect跳转,根据上一保存请求进行成功跳转 .defaultSuccessUrl("/index") // 认证成功 forward跳转,始终在认证成功之后跳转到指定请求 // .successForwardUrl("/index") // .failureForwardUrl("/") /* .successHandler((request, response, exception)->{ response.sendRedirect("/index"); })*/ .failureHandler((request, response, exception) -> { request.setAttribute("msg",exception.getMessage()); request.getRequestDispatcher("/").forward(request,response); }) .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .and() .csrf().disable() ; return http.build(); } }
这里需要注意的是formLogin认证失败后将不在使用failureForwardUrl()方法转发,而是使用failureHandler处理器方式处理错误信息并跳转页面。
-
如果使用
failureForwardUrl()方式,请到ForwardAuthenticationFailureHandler源码中查看错误信息封装:

SpringSecurity将认证错误信息保存到Request作用域中,并取名为SPRING_SECURITY_LAST_EXCEPTION,所以直接可以到前端页面使用${SPRING_SECURITY_LAST_EXCEPTION}方式进行获取展示。
-
如果使用
failureHandler处理器方式,则可以自定义错误页面及错误信息:
.failureHandler((request, response, exception) -> {
//将认证错误信息保存到request作用域,取名为msg
request.setAttribute("msg",exception.getMessage());
//认证失败后转发到指定页面
request.getRequestDispatcher("/").forward(request,response);
})
本次案例使用的是自定义failureHandler处理器方式,修改登录页面login.ftl加入错误信息展示区域:
<div>${msg!}</div>
2.5.启动测试
在sys_user表中添加测试用户信息,此处请使用明文密码方式进行身份验证。
修改IndexController中的跳转到首页的方法,加入获取SpringSecurity认证信息并带入前段页面进行展示。
@RequestMapping("/index")
public String toIndex(Model model){
//获取Spring Security认证成功后的相关信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//将认证信息转换成JSON数据
String json = JSON.toJSONString(authentication);
//存入数据模型中带入前端页面进行展示
model.addAttribute("auth",json);
return "index";
}
启动项目并进行登录测试。
在index.ftl首页中通过${auth}展示SpringSecurity认证成功的相关信息。

3.密码方式
Spring Security提供了多种密码加密方式,大致可以归类于以下几种:
-
对密码进行明文处理,即不采用任何加密方式;
-
采用MD5加密方式;
-
采用哈希算法加密方式;
3.1.自定义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加密密码保存到数据库表中。
3.2.BCryptPasswordEncoder密码编码器
BCryptPasswordEncoder是Spring Security中一种基于bcrypt算法的密码加密方式。bcrypt算法是一种密码哈希函数,具有防止彩虹表攻击的优点,因此安全性较高。
使用BCryptPasswordEncoder进行密码加密时,可以指定一个随机生成的salt值,将其与原始密码一起进行哈希计算。salt值可以增加密码的安全性,因为即使两个用户使用相同的密码,由于使用不同的salt值进行哈希计算,得到的哈希值也是不同的。
在Spring Security中,可以通过在SecurityConfig配置类中添加以下代码来使用BCryptPasswordEncoder进行密码加密:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
这样就可以在Spring Security中使用BCryptPasswordEncoder进行密码加密了。
4.RememberMe
在实际开发中,为了用户登录方便常常会启用记住我(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 过滤器,通过该过滤器实现自动登录。
@Configuration @EnableWebSecurity public class WebSecurityConfig { //省略其他配置,参考之前 @Autowired public UserDetailsService userDetailsService; @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; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/") .loginProcessingUrl("/user/userLogin") .passwordParameter("password") .usernameParameter("username") // 认证成功 redirect跳转,根据上一保存请求进行成功跳转 .defaultSuccessUrl("/index") // 认证成功 forward跳转,始终在认证成功之后跳转到指定请求 //.successForwardUrl("/index") //failureForwardUrl("/") /*.successHandler((request, response, exception)->{ response.sendRedirect("/index"); })*/ .failureHandler((request, response, exception) -> { request.setAttribute("msg",exception.getMessage()); request.getRequestDispatcher("/").forward(request,response); }) .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) .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .and() .csrf().disable() ; return http.build(); } }
rememberMe主要方法介绍:
| 方法 | 说明 |
|---|---|
rememberMeParameter() | 指定在登录时“记住我”的 HTTP 参数,默认为 remember-me |
tokenValiditySeconds() | 设置 Token 有效期为 200s,默认时长为 2 星期 |
tokenRepository() | 指定 rememberMe 的 token 存储方式,可以使用默认的 PersistentTokenRepository 或自定义的实现 |
userDetailsService() | 指定 UserDetailsService 对象 |
rememberMeCookieName() | 指定 rememberMe 的 cookie 名称 |
-
修改登录页面
login.ftl,添加remember-Me记住我的checkbox选项框。
<form action="/user/userLogin" method="post"> <label>账号:</label><input type="text" name="username"/><br/> <label>密码:</label><input type="password" name="password"/><br/> <input type="checkbox" name="remember-me"/>记住我<br/> <input type="submit" value="登 录"/> </form>
注意:配置的
checkbox复选框的name属性名要与上面配置的rememberMeParameter("属性名")一致,默认就叫remember-me。
总结:remember-me 只有在 JSESSIONID 失效和SecurityContextPersistenceFilter 过滤器认证失败或者未进行认证时才发挥作用。此时,只要 remember-me 的 Cookie 不过期,我们就不需要填写登录表单,就能实现再次登录,并且 remember-me 自动登录成功之后,会生成新的 Token 替换旧的 Token,相应 Cookie 的 Max-Age 也会重置。
5.CSRF防御
5.1.什么是CSRF
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种利用用户已登录的身份在用户不知情的情况下发送恶意请求的攻击方式。攻击者可以通过构造恶意链接或者伪造表单提交等方式,让用户在不知情的情况下执行某些操作,例如修改密码、转账、发表评论等。为了防范
CSRF攻击,常见的做法是在请求中添加一个CSRF Token(也叫做同步令牌、防伪标志),并在服务器端进行验证。CSRF Token是一个随机生成的字符串,每次请求都会随着请求一起发送到服务器端,服务器端会对这个Token进行验证,如果Token不正确,则拒绝执行请求。
5.2.SpringSecurity中如何使用CSRF
在
Spring Security中,防范CSRF攻击可以通过启用CSRF保护来实现。启用CSRF保护后,Spring Security会自动在每个表单中添加一个隐藏的CSRF Token字段,并在服务器端进行验证。如果Token验证失败,则会抛出异常,从而拒绝执行请求。启用CSRF保护的方式是在Spring Security配置文件中添加.csrf()方法,例如:http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());在上面的配置中,我们使用了
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保护。 |
重启项目进行测试。
-
问题一:开启了
SpringSecurity的CSRF防御之后导致登录的POST请求失败?
原因分析:使用了 spring-security 后,默认开启了防止跨域攻击的功能,任何 POST 提交到后台的表单都要验证是否带有 _csrf 参数,一旦传来的 _csrf 参数不正确,服务器便返回 403 错误。
解决方案:修改login.ftl页面代码,加入_csrf隐藏域。
<form action="/user/userLogin" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<label>账号:</label><input type="text" name="username"/><br/>
<label>密码:</label><input type="password" name="password"/><br/>
<input type="checkbox" name="remember-me"/>记住我<br/>
<input type="submit" value="登 录"/>
</form>
如果针对一些特定的请求接口,不需要进行CSRF防御,可以通过以下配置忽略:
http.csrf().ignoringAntMatchers("/upload"); // 禁用/upload接口的CSRF防御
-
问题二:
SpringSecurity退出登录/logout提示404问题
退出登录logout实现方式:
http.logout().logoutUrl("/logout").
logoutSuccessUrl("/").permitAll();
结果运行项目,执行安全退出时提示页面404错误。
错误原因:SpringSecurity3.2开始,默认会启动CSRF防护,一旦启动了CSRF防护,“/logout” 需要用post的方式提交,SpringSecurity才能过滤。
-
方式一:配置文件直接关闭
CSRF防护
http.csrf().disable(); //关闭csrf防护
注意:当关闭CSRF防护时,部分的页面可能会无法刷新,甚至报错。这时可以在页面的<header>标签之间加入以下代码:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
-
方式二:官方建议使用
POST请求退出登陆,并携带CRSF令牌
http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")).
logoutSuccessUrl("/").permitAll();















![概率中的50个具有挑战性的问题[02/50]:连续获胜](https://img-blog.csdnimg.cn/img_convert/fab2aae23fd92fec5d5018ff37b32af2.png)
![[c]扫雷](https://img-blog.csdnimg.cn/img_convert/b1a6522aec2533493b917d7ee6d245cd.png)


