SpringBoot 学习笔记_安全管理 —— Spring Security 配置
声明:
本次学习参考 《SpringBoot + Vue 开发实战》 · 王松(著) 一书。
本文的目的是记录我学习的过程和遇到的一些问题以及解决办法,其内容主要来源于原书。
如有侵权,请联系我删除
文章目录
- SpringBoot 学习笔记_安全管理 —— Spring Security 配置
- SpringBoot 安全管理 —— Spring Security 基本配置
- 基本用法
- 配置用户名和密码
- 基于内存的认证
- HttpSecurity
- 登录表单详细配置
- 注销登录配置
- 多个 HttpSecurity
- 密码加密
- 方法安全
Java 中常见的安全框架有 Shiro 和 Spring Security。 Shiro 是一个轻量级安全管理框架,提供了认证、授权、会话管理、密码管理、缓存管理等功能;Spring Security 是一个相对复杂的安全管理框架,功能比 Shiro 更加强大,权限控制细粒度更高,对 OAuth 2 的支持也更友好,其源于 Spring 家族,和 Spring 框架可以无缝整合,且 SpringBoot 也提供了自动化配置方案。
SpringBoot 安全管理 —— Spring Security 基本配置
基本用法
- 创建 SpringBoot 项目,添加依赖
<!-- 添加 Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 添加 Controller
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello";
}
}
- 启动项目测试
http://localhost:8080/login
会自动跳转到 Spring Security 提供的登录页面。默认用户名user
, 默认密码随机生成在启动日志中(如:Using generated security password: 3d3cfaef-cde0-40c6-a023-0be1bb9a7fe4
)。
配置用户名和密码
默认的用户名为 user, 默认密码为随机生成的。当然,也可以在
applicatioin.properties
中配置默认的用户名和密码及角色
spring.security.user.name=sang
spring.security.user.password=123456
spring.secutity.user.roles=admin
基于内存的认证
也可以自定义类继承
WebSecurityConfigurerAdapter
,进而对Spring Security
更多自定义配置。
@Configuration
// 自定义 MyWebSecurityConfig 继承 WebSecurityConfigurerAdapter
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
// 引入密码的加密方式。必须指定一种。
@Bean
PasswordEncoder passwordEncoder() {
// 这里使用 NoOpPasswordEncode,即不对密码进行加密。该方法因安全性不足够已被标记为过时
return NoOpPasswordEncoder.getInstance();
}
// 重写 configure 方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 基于内存的用户配置角色时不需要加 "ROLE_" 前缀
// 用户一:用户名:root, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("root").password("123456").roles("ADMIN", "DBA")
.and()
// 用户二:用户名:admin, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("admin").password("123456").roles("ADMIN", "USER")
.and()
// 用户三:用户名:sang, 密码 123456, 具备 USER 角色
.withUser("sang").password("123").roles("USER");
}
}
随着安全性的要求不断提高,之前默认的密码编码器
NoOpPasswordEncoder
已经不被推荐了,取代它的是DelegatingPasswordEncoder
,具体区别推荐访问如果返回
Bad credentials
错误,一般是用户名和密码错误导致,如果密码有设定加密规则,也需要考虑。
HttpSecurity
如上配置已经可以实现认证功能,但是受保护的资源都是默认的,还不能根据实际情况进行角色管理。如果想实现特定的管理,需要重写
WebSecurityConfigurerAdapter
中的另一个方法
@Configuration
// 自定义 MyWebSecurityConfig 继承 WebSecurityConfigurerAdapter
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
// 引入密码的加密方式。必须指定一种。
@Bean
PasswordEncoder passwordEncoder() {
// 这里使用 NoOpPasswordEncode,即不对密码进行加密。该方法因安全性不足够已被标记为过时
return NoOpPasswordEncoder.getInstance();
//return new DelegatingPasswordEncoder(null, null);
}
/**
* 重写 configure 方法,配置用户角色
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 基于内存的用户配置角色时不需要加 "ROLE_" 前缀
// 用户一:用户名:root, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("root").password("123456").roles("ADMIN", "DBA")
.and()
// 用户二:用户名:admin, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("admin").password("123456").roles("ADMIN", "USER")
.and()
// 用户三:用户名:sang, 密码 123456, 具备 USER 角色
.withUser("sang").password("123").roles("USER");
}
/**
* 重写 configure 方法,根据实际情况配置角色与受保护资源关系
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启 HttpSecurity 的配置
http.authorizeRequests()
// 用户访问 /admin/** 模式的 URL 必须具备 ADMIN 角色
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 用户访问 /user/** 模式的 URL 必须具备 ADMIN 或者 USER 角色
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN', 'USER')")
// 用户访问 /db/** 模式的 URL 必须具备 ADMIN 和 DBA 角色
.antMatchers("/db/**")
.access("hasRole('ADMIN') and hasRole('DBA')")
// 除了以上指定的 URL 模式之外,用户访问其他 URL 必须认证(登录)后
.anyRequest()
.authenticated()
// 开启表单登录。即默认的登录页面
.and()
.formLogin()
// 配置登录接口为 /login 即可以直接调用 /login 接口发起 POST 请求进行登录。用户名命名为 username,密码命令为 password
.loginProcessingUrl("/login")
// 表示和登录相关接口不需要认证即可访问
.permitAll()
// 关闭 CSRF
.and()
.csrf()
.disable();
}
}
接下来,在 Controller 中添加接口测试
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin(){
return "Hello admin";
}
@GetMapping("/user/hello")
public String user(){
return "Hello user";
}
@GetMapping("/db/hello")
public String dba(){
return "Hello dba";
}
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
登录表单详细配置
上面的配置,一直使用的是 Spring Security 的默认登录界面,登录成功后也是默认跳转。在前后端分离的模式中,登录成功后就不需要默认的跳转,而是需要返回 JSON 数据。要实现该功能,需要继续完善配置
@Configuration
// 自定义 MyWebSecurityConfig 继承 WebSecurityConfigurerAdapter
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
// 引入密码的加密方式。必须指定一种。
@Bean
PasswordEncoder passwordEncoder() {
// 这里使用 NoOpPasswordEncode,即不对密码进行加密。该方法因安全性不足够已被标记为过时
return NoOpPasswordEncoder.getInstance();
//return new DelegatingPasswordEncoder(null, null);
}
/**
* 重写 configure 方法,配置用户角色
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 基于内存的用户配置角色时不需要加 "ROLE_" 前缀
// 用户一:用户名:root, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("root").password("123456").roles("ADMIN", "DBA")
.and()
// 用户二:用户名:admin, 密码 123456, 具备 ADMIN 和 DBA 角色
.withUser("admin").password("123456").roles("ADMIN", "USER")
.and()
// 用户三:用户名:sang, 密码 123456, 具备 USER 角色
.withUser("sang").password("123456").roles("USER");
}
/**
* 重写 configure 方法,根据实际情况配置角色与受保护资源关系
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启 HttpSecurity 的配置
http.authorizeRequests()
// 用户访问 /admin/** 模式的 URL 必须具备 ADMIN 角色
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 用户访问 /user/** 模式的 URL 必须具备 ADMIN 或者 USER 角色
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN', 'USER')")
// 用户访问 /db/** 模式的 URL 必须具备 ADMIN 和 DBA 角色
.antMatchers("/db/**")
.access("hasRole('ADMIN') and hasRole('DBA')")
// 除了以上指定的 URL 模式之外,用户访问其他 URL 必须认证(登录)后
.anyRequest()
.authenticated()
/* ******* 详细的配置 ******* */
.and()
// 开启表单登录
.formLogin()
// 指定登录页面
.loginPage("/login_page")
// 指定登录请求处理接口
.loginProcessingUrl("/login")
// 自定义认证所需用户名参数名。默认 username
.usernameParameter("username")
// 自定义认证所需密码参数名。默认 password
.passwordParameter("password")
// 自定义登录成功处理逻辑
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse res,
Authentication auth)
throws IOException {
Object principal = auth.getPrincipal();
res.setContentType("application/json;charset=utf-8");
PrintWriter out = res.getWriter();
res.setStatus(200);
Map<String, Object> map = new HashMap<>();
map.put("status", 200);
map.put("msg", principal);
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(map));
out.flush();
out.close();
}
})
// 自定义登录失败处理逻辑
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req,
HttpServletResponse res,
AuthenticationException e)
throws IOException, ServletException {
res.setContentType("application/json;charset=utf-8");
PrintWriter out = res.getWriter();
res.setStatus(401);
Map<String, Object> map = new HashMap<>();
map.put("status", 401);
if (e instanceof LockedException)
map.put("msg", "账户被锁定,登录失败");
else if (e instanceof BadCredentialsException)
map.put("msg", "账户名或密码输入错误,登录失败");
else if (e instanceof DisabledException)
map.put("msg", "账户被禁用,登录失败");
else if (e instanceof AccountExpiredException)
map.put("msg", "账户已过期,登录失败");
else if (e instanceof CredentialsExpiredException)
map.put("msg", "密码已过期,登录失败");
else
map.put("msg", "登录失败");
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(map));
out.flush();
out.close();
}
})
//登录相关接口不需要认证即可访问
.permitAll()
// 关闭 CSRF
.and()
.csrf()
.disable();
/* ******* 详细的配置 ******* */
}
}
使用 postman 测试
注销登录配置
只需要继续在上面配置文件中追加配置
...
// 注销登录的相关配置
.and()
// 开启注销登录配置
.logout()
// 配置注销登录请求 URL,默认 /logout
.logoutUrl("/logout")
// 清除身份认证信息,默认 true
.clearAuthentication(true)
// 使 Session 失效,默认 true
.invalidateHttpSession(true)
// 自定义注销时的操作。如 清除 Cookies 等,Spring Security 默认有提供一些常见实现
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest req,
HttpServletResponse res,
Authentication auth) {
}
})
// 自定义注销成功后的操作。如返回 JSON 或页面跳转。
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req,
HttpServletResponse res,
Authentication auth) throws IOException, ServletException {
// 跳转到登录页面
res.sendRedirect("/login_page");
}
})
...
多个 HttpSecurity
在一些复杂业务中,可以配置多个
HttpSecurity
实现对WebSecurityConfigurerAdapter
的多次扩展
- 配置多个
HttpSecurity
时,配置类不需要继承WebSecurityConfigurerAdapter
,只需在配置类内部创建静态内部类继承WebSecurityConfigurerAdapter
,然后加上@Configuration
注解和@Order
注解。@Order
注解表示优先级,值越小,优先级越高
@Configuration
public class MultiHttpSecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Autowired
public void configure (AuthenticationManagerBuilder auth) throws Excpetion {
auth.inMemoryAuthentication()
.withUser("admin").password("123456").roles("ADMIN")
.and()
.withUser("sang").password("123456").roles("USER");
}
/**
* /admin/** 模式 URL 处理
*/
@Configuration
@Order(1)
public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity http) throws Exception {
http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("ADMIN");
}
}
/**
* 其他模式 URL 处理
*/
@Configuration
@Order(2)
public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
}
}
密码加密
前面的所有例子都是使用了
NoOpPasswordEncoder.getInstance()
密码编码器,也提到过该方法是不进行加密的,因为安全性低,已经被标记为过时。
想要对密码进行加密,可以在 Config 文件中配置密码加密规则,如 MD5 加密、BCrypt 强哈希加密等等,如:
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10); // 这里 10 表示密钥迭代次数,默认为 10
}
同时,配置内存中的用户密码也不能再是明文的 123456 了,而要改为密文。
但是,实际项目中,大多情况下用户都是通过手动注册,然后将密码存储到数据库中,这种场景,最方便的就是创建一个 Service 用来对用户信息加密,并存储到 DB
@Service
public class RegService {
public int reg(String username, String password){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
String encodePassword = encoder.encode(password);
// 这里将加密后的用户名和密文保存到数据库。返回保存是否成功标识。
return 1;
}
}
方法安全
上述的认证和授权都是基于 URL 的,有些需求可能会要求认证精确到某个方法,所以就需要使用
@EnableGlobalMethodSecurity
注解开启基于注解的安全配置。
@Configuration
// 开启基于注解的安全配置
// prePostEnable = true 解锁 @PreAuthorize 和 @PostAuthorize 注解。分别是在方法执行前和方法执行后进行验证。
// securedEnabled = true 解锁 @Secured 注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}
开启注解安全配置后,创建一个 MethodService 进行测试
@Service
public class MethodService {
// 访问该方法需要 ADMIN 角色。这里注意角色前需要前缀 ROLE_
@Secured("ROLE_ADMIN")
public String admin(){
return "Hello Admin";
}
// 访问该方法需要 ADMIN 和 DBA 双角色。
@PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
public String dba(){
return "Hello DBA";
}
// 访问该方法需要 ADMIN 或者 DBA 或者 USER 角色
@PreAuthorize("hasAnyRole('ADMIN', 'DBA', 'USER')")
public String user(){
return "Hello USER";
}
}
创建 Controller 测试
@RestController
public class MethodController {
@Autowired
MethodService service;
@GetMapping("/method/admin")
public String admin(){
return service.admin();
}
@GetMapping("/method/dba")
public String dba(){
return service.dba();
}
@GetMapping("/method/user")
public String user(){
return service.user();
}
}
使用 sang 用户登录(角色为 USER),访问 /method/admin
和 /method/dba
返回 403 错误,访问 /method/user
返回 Hello 字样,说明配置成功。
- 如果遇到项目启动后无法找到页面或重定向次数过多错误:
- 先检查是否配置了自定义的登录页面:
.loginPage("/login_page")
却没有创建该html
文件,可以自定义创建登录页面的html
文件,或者暂时注释掉该行,使用 Spring Security 提供的默认登录页面