上一篇通过一个入门案例引入Spring Security
,然后居于内存进行登录用户定制。本文来学习用户认证定制和用户授权相关知识。自带的登录认证与登录拦截存在很多不方便,开发中一般不使用默认认证,往往需要根据项目定制。
一、用户认证定制
开发中,项目都有自己的登录页面,用不上默认的登录页面,而且默认的登录页面样式也不好看,这时就需要自己定制。一但定制后,原先的页面默认自动认证都会失效,都需要重新定制。
1.1、定制登录页面
步骤1: 定制登陆页面/resources/static/login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org/" lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form action="/login" method="post">
用户名:<input type="text" name="username"> <br>
密码:<input type="text" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
步骤2: 修改 SecurityConfig
继承 WebSecurityConfigurerAdapter
,重写 configure(HttpSecurity http)
。
package com.duan.config;
import com.duan.handler.MyAuthenticationFailureHandler;
import com.duan.handler.MyAuthenticationSuccessHandler;
import com.duan.handler.MyLogoutSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @author db
* @version 1.0
* @description SecurityConfig
* @since 2024/7/15
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("p1").build());
manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用csrf保护
http.csrf().disable();
// 请求url权限控制
http.authorizeRequests()
//匹配 /login.html 路径,permitAll() 不限制(登录/不登录都可以访问该路径,不进行登录检查)
.antMatchers("/login.html").permitAll()
.antMatchers("/login").permitAll()
//除去上面匹配路径外,剩下的路径都需要进行登录检查
.anyRequest().authenticated();
//用户登录控制
http.formLogin()
//指定登录页面
.loginPage("/login.html")
//指定登录路径
.loginProcessingUrl("/login");
}
}
API解释
- http.csrf().disable(): 禁用 csrf 保护;Spring security为防止CSRF(Cross-site request forgery跨站请求伪造) 的发生,限制了除了GET以外的大多数方法。
- http.authorizeRequests():请求url权限控制,可以进行路径,权限配置。
- http.formLogin(): 用户登录控制,控制登录相关操作。
启动项目,浏览器访问http://localhost:8183/testMethod
,界面跳到定制的登陆页面。
1.2、定制登录成功跳转路径
Spring Security
登录成功之后,默认跳转到上一个路径,如果需要定制跳转路径,可以使用下面配置。
//用户登录控制
http.formLogin()
.successForwardUrl("/success")
在 controller
中新建 success
方法。
@RequestMapping("/success")
public String success(){
return "success";
}
测试:登录成功观察跳转。
1.3、定制登录失败跳转路径
Spring Security
登录失败之后,默认跳转到登录页面,如果需要定制跳转路径,可以使用下面配置。
//用户登录控制
http.formLogin()
.failureForwardUrl("/fail")
登录失败,跳转/fail
路径,因为没有登录,需要放行/fail
登录检查。
http.authorizeRequests()
.antMatchers("/fail").permitAll()
在 controller
中新建 fail
方法。
@RequestMapping("/fail")
public String fail(){
return "fail";
}
测试:登录失败观察跳转。
1.4、定制登录成功逻辑
前面定制登录成功后使用successForwardUrl
跳转某个接口路径,如果是前后端分离项目,要求返回是Json
格式,那怎么办?此时可以使用登录成功处理器。
新建 MyAuthenticationSuccessHandler
类实现AuthenticationSuccessHandler
。
package com.duan.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author db
* @version 1.0
* @description MyAuthenticationSuccessHandler
* @since 2024/7/20
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//返回json
response.setContentType("application/json;charset=utf-8");
String data = "{\"code\":200, \"msg\":\"登录成功\", \"data\":{}}";
response.getWriter().write(data);
}
}
securityConfig
中配置登录成功处理器。
//用户登录控制
http.formLogin()
.successHandler(new MyAuthenticationSuccessHandler());
测试:登录成功,观察效果。
1.5、定制登陆失败逻辑
与登录成功处理器对比,也有登录失败处理器。
新建MyAuthenticationFailureHandler
类实现AuthenticationFailureHandler
。
package com.duan.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author db
* @version 1.0
* @description MyAuthenticationFailureHandler
* @since 2024/7/20
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//返回json
response.setContentType("application/json;charset=utf-8");
String data = "{\"code\":500, \"msg\":\"登录失败\", \"data\":{\"error\":"+exception.getMessage()+"}}";
response.getWriter().write(data);
}
}
securityConfig
中配置登录成功处理器。
//用户登录控制
http.formLogin()
.failureHandler(new MyAuthenticationFailureHandler())
测试:登录失败,观察效果。
1.6、定制登出成功跳转路径
Spring Security
登录成功之后,默认跳转登录页面,如果有需要可以定制跳转路径。
//用户登出控制
http.logout()
.logoutSuccessUrl("/logoutsuccess")
登出之后,logoutsuccess
要放行。
//请求url权限控制
http.authorizeRequests()
.antMatchers("/logoutsuccess").permitAll()
.anyRequest().authenticated();
在controller
中新建logoutsuccess
方法。
@RequestMapping("/logoutSuccess")
public String logoutsuccess(){
return "logoutSuccess";
}
测试:登出成功,观察效果。
1.7、定制登出成功逻辑
登录成功有逻辑定制,登出成功也可以定制。
新建MyLogoutSuccessHandler
实现LogoutSuccessHandler
。
package com.duan.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author db
* @version 1.0
* @description MyLogoutSuccessHandler
* @since 2024/7/20
*/
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//返回json
response.setContentType("application/json;charset=utf-8");
String data = "{\"code\":200, \"msg\":\"登出成功\", \"data\":{}}";
response.getWriter().write(data);
}
}
配置登出。
//用户登出控制
http.logout()
.logoutSuccessHandler(new MyLogoutSuccessHandler())
二、用户授权
Spring Security
登录成功之后第二步就授权,所以要先学习一下 Spring Security
中是怎么进行用户授权的,学习之前,先了解一下基于角色的权限管理,它有助于理解Spring Security
中的用户授权。
2.1、RBAC概念
基于角色的访问控制(Role-Based Access Control,简称RBAC
),主要是通过将权限分配给角色,再将角色分配给用户,简化权限管理的复杂性。RBAC权限管理是我们常见的一种方式。
- 用户(User):指的是系统中的用户,每个用户可以被分配一个或者多个角色。
- 角色(Role):指的是权限的集合,一个角色包含多种操作权限。
- 权限(Permission):指的是对系统资源进行特定的操作,比如用户增加、用户修改等。
实现原理: 在Javaweb
中权限的控制,其实就是对请求映射方法控制,如果登录用户有这个权限,允许访问,如果没有权限,不允许访问。
基于这个原理,RBAC
实现步骤如下
- 自定义一个权限注解:
@PermissionAnno
,约定方法上有这个注解的,必须进行权限校验。 - 在需要进行权限校验的请求映射方法中增加这个注解。
- 使用拦截器对所有请求进行拦截,每次访问进行权限校验。
- 如果使用admin登录,不需要拦截。
Spring Security
支持RBAC
授权,所以授权原理是一样的。具体代码实现方案有2种,一种为配置方式,一种为注解方式。
2.2、基于配置方式授权
权限配置授权以用户CURD
操作为例进行学习。
步骤1: 定义用户CURD4
个接口
package com.duan.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/9/2
*/
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("/insert")
public String insert(){
return "user-insert";
}
@GetMapping("/update")
public String update(){
return "user-update";
}
@GetMapping("/delete")
public String delete(){
return "user-delete";
}
@GetMapping("/list")
public String list(){
return "user-list";
}
}
步骤2: 在项目SecurityConfig
中配置4个接口访问权限。
// 请求url权限控制
http.authorizeRequests()
.antMatchers("/user/insert").hasAuthority("user:insert")
.antMatchers("/user/update").hasAuthority("user:update")
.antMatchers("/user/delete").hasAuthority("user:delete")
.antMatchers("/user/list").hasAuthority("user:list")
.anyRequest().authenticated();
// 表示访问/user/list接口只需要有user:list或者user:query权限即可
.antMatchers("/user/list").hasAnyAuthority("user:list", "user:query")
步骤3: 配置用户权限在项目SecurityConfig
中配置用户权限,cxykk
用户有"user:insert", "user:update"
权限。
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("user:insert", "user:update").build());
manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
return manager;
}
步骤4: 测试权限校验前需要先登录,使用cxykk用户登录, 登录成功后访问下面2组接口。
访问http://localhost:8183/user/delete和http://localhost:8183/user/delete报错。
角色配置授权
以用户CURD
操作为例进行学习。
步骤1: 定义用户CURD4
个接口。
package com.duan.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/9/2
*/
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("/insert")
public String insert(){
return "user-insert";
}
@GetMapping("/update")
public String update(){
return "user-update";
}
@GetMapping("/delete")
public String delete(){
return "user-delete";
}
@GetMapping("/list")
public String list(){
return "user-list";
}
}
步骤2: 4个接口运行访问的配置。
// 请求url权限控制
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/fail").permitAll()
.antMatchers("/logoutSuccess").permitAll()
// .antMatchers("/user/insert").hasAuthority("user:insert")
// .antMatchers("/user/update").hasAuthority("user:update")
// .antMatchers("/user/delete").hasAuthority("user:delete")
// .antMatchers("/user/list").hasAuthority("user:list")
.antMatchers("/user/insert").hasRole("user_mag")
.antMatchers("/user/update").hasRole("user_mag")
.antMatchers("/user/delete").hasAuthority("hr")
.antMatchers("/user/list").hasAuthority("hr")
.anyRequest().authenticated();
// 表示访问 **"/user/list"** 接口只需要拥有 **"user_mag"** 或 **"hr"**角色
.antMatchers("/user/list").hasAnyRole("user_mag", "hr")
步骤3: 配置用户权限
在项目SecurityConfig
中配置用户权限,cxykk
用户有"user:insert", "user:update"
权限。
步骤4: 测试
权限校验前需要先登录,使用cxykk
用户登录, 登录成功后访问下面2组接口。
访问http://localhost:8183/user/delete
和http://localhost:8183/user/delete
报错。
2.3、基于注解方式授权
上面的配置方式比较简单,开发中很少使用,当接口多了之后,一个个配置非常麻烦,Spring Security2.X
中存在注解方式,简化操作配置。
Spring Security
注解方式授权与鉴权涉及到2个核心的注解:@PreAuthorize @Secured
其中@PreAuthorize
是基于权限授权(也可以基于角色),@Secured
是基于角色授权,先看基于权限授权。权限注解方式以用户CURD
操作为例进行学习。
步骤1、 配置启动注解
默认情况下Spring Security
是不支持注解鉴权的,此时需要配置类中开启。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
步骤2: 定义用户CURD4
个接口注意:使用注解标记方法接口需要的权限。
package com.duan.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/9/2
*/
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("/insert")
@PreAuthorize("hasAnyAuthority('user:insert')")
public String insert(){
return "user-insert";
}
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('user:update')")
public String update(){
return "user-update";
}
@GetMapping("/delete")
@PreAuthorize("hasAnyAuthority('user:delete')")
public String delete(){
return "user-delete";
}
@GetMapping("/list")
@PreAuthorize("hasAnyAuthority('user:list')")
public String list(){
return "user-list";
}
}
步骤3: 配置用户拥有的权限。
//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).authorities("user:insert", "user:update").build());
manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).authorities("p2").build());
return manager;
}
步骤4: 移除http.authorizeRequests
中权限配置。
// 请求url权限控制
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/fail").permitAll()
.antMatchers("/logoutSuccess").permitAll()
// .antMatchers("/user/insert").hasAuthority("user:insert")
// .antMatchers("/user/update").hasAuthority("user:update")
// .antMatchers("/user/delete").hasAuthority("user:delete")
// .antMatchers("/user/list").hasAuthority("user:list")
.anyRequest().authenticated();
步骤5: 测试
权限校验前需要先登录,使用cxykk
用户登录, 登录成功后访问下面2组接口。
角色注解授权
需求: 以用户CRUD
操作为例子-注解-角色
步骤1: 配置启动注解
默认情况下Spring Security
是不支持注解鉴权的,此时需要配置类中开启。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
步骤2:定义用户crud4个接口注意:使用注解标记方法接口需要的权限。
package com.duan.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/9/2
*/
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("/insert")
// @PreAuthorize("hasAnyAuthority('user:insert')")
@PreAuthorize("hasAnyRole('user_mag')")
public String insert(){
return "user-insert";
}
@GetMapping("/update")
// @PreAuthorize("hasAnyAuthority('user:update')")
@PreAuthorize("hasAnyRole('user_mag')")
public String update(){
return "user-update";
}
@GetMapping("/delete")
// @PreAuthorize("hasAnyAuthority('user:delete')")
@PreAuthorize("hasAnyRole('hr')")
public String delete(){
return "user-delete";
}
@GetMapping("/list")
// @PreAuthorize("hasAnyAuthority('user:list')")
@PreAuthorize("hasAnyRole('hr')")
public String list(){
return "user-list";
}
}
步骤3: 配置用户拥有的权限。
//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("cxykk").password(passwordEncoder().encode("123qwe")).roles("user_mag").build());
manager.createUser(User.withUsername("cxydb").password(passwordEncoder().encode("123qaz")).roles("p2").build());
return manager;
}
步骤4: 移除http.authorizeRequests中权限配置。
// 请求url权限控制
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/fail").permitAll()
.antMatchers("/logoutSuccess").permitAll()
// .antMatchers("/user/insert").hasRole("user_mag")
// .antMatchers("/user/update").hasRole("user_mag")
// .antMatchers("/user/delete").hasAuthority("hr")
// .antMatchers("/user/list").hasAuthority("hr")
.anyRequest().authenticated();
步骤5: 测试
权限校验前需要先登录,使用cxykk
用户登录, 登录成功后访问下面2组接口。
@Secured
注解也是可以授权的,只不过是它不支持SpEL表达式,只能指定特定角色,如果用户没有满足注解内指定的角色之一,方法调用会被拒绝。在这里就不细讲了。
三、权限异常处理
当授权出现异常时,默认返回的是403页面,可以通过exceptionHandling
进行控制。
页面跳转
步骤: 定义nopermission.html
。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org/" lang="en">
<head>
<meta charset="UTF-8">
<title>没有权限</title>
</head>
<body>
<h1>没有权限</h1>
</body>
</html>
步骤2: 配置权限异常跳转页面。
//异常控制
http.exceptionHandling()
.accessDeniedPage("/nopermission.html");
Json
格式返回
步骤1: 定义权限异常处理器。
package com.duan.handler;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author db
* @version 1.0
* @description MyAccessDeniedHandler
* @since 2024/9/5
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//返回json
response.setContentType("application/json;charset=utf-8");
String data = "{\"code\":403, \"msg\":\"没有权限\", \"data\":{\"error\":"+accessDeniedException.getMessage()+"}}";
response.getWriter().write(data);
}
}
步骤2: 配置权限异常处理器。
//异常控制
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler())
代码地址:https://gitee.com/duan138/practice-code/tree/dev/springsecurity
四、总结
通过配置方式去加深了解Spring Security
中用户认证和用户授权,虽然在项目中不是通过这种配置方式去进行编码,都是从数据库中进行用户认证和授权,但是了解这些之后,再去通过数据库去进行认证和授权,将会简单很多,先了解基础,然后进行实践。
下篇文章将要进行实战,通过SpringBoot + Spring Security + JWT + Mybaties-plus + Redis
进行数据库级别的认证和授权。
改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。