文章目录
- 基本概念介绍
- 项目搭建
- 内存认证
- 自定义认证逻辑
- 密码解析器PasswordEncoder
- 自定义登录界面
- 会话管理
- 授权
- 授权__权限表设计
- 编写查询权限的方法
- 配置类设置访问控制
- 自定义访问控制逻辑
- 注解设置访问控制
- 在前端进行访问控制
- 总结
基本概念介绍
源码:https://gitee.com/DoubleW2w/spring-security-learning
介绍
SpringSecurity 安全服务框架,核心功能是认证和授权。提供了生命是安全访问控制功能,减少为了系统安全而编写大量重复代码的工作。
认证
认证即判断用户的身份是否合法,合法则继续访问,不合法则拒绝访问。
- 用户密码登录,二维码登录,手机短信登录,人脸识别,指纹认证
目的:保护系统隐私数据与资源
授权
认证通过后,根据用户的权限来控制用户访问资源的过程。普通用户只能看普通资源,VIP用户能看VIP资源。
认证是保证用户身份的合法性,授权则是为了更细粒度对数据进行划分,控制不同的用户能够访问不同的资源
项目搭建
依赖pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
<!-- lombok -->
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
package com.doublew2w.controller;
@Controller
public class PageController {
@RequestMapping("/{page}")
public String showPage(@PathVariable String page) {
return page;
}
}
package com.doublew2w;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
resources.templates
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录首页</title>
</head>
<body>
<h1>主页面</h1>
</body>
</html>
resources
server:
port: 80
#日志格式
logging:
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss, SSS} %clr(%-5level) --- [%thread] %cyan(%-50logger{50}):%msg%n'
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mysecurity?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
启动项目,访问项目主页面 http://localhost/main
,项目会自动跳转到一个登录页面。这代表Spring Security已经开启了认证
功能,不登录无法访问所有资源,该页面就是 Spring Security 自带的登录页面。
我们使用 user 作为用户名,控制台中的字符串作为密码登录,登录成功后跳转到项目主页面。
内存认证
Spring Security 会将登录页传来的用户名密码和内存中的用户名密码做匹配认证,使用到的类是 InMemoryUserDetailsManager
@Configuration
public class SecurityConfig {
/**
* 定义认证逻辑
*
* @return 认证逻辑Bean
*/
@Bean
public UserDetailsService userDetailsService() {
// 1.使用内存数据进行认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 2.创建两个用户
UserDetails user1 = User.withUsername("baizhan").password("123").authorities("admin").build();
UserDetails user2 = User.withUsername("sxt").password("456").authorities("admin").build();
// 3.将这两个用户添加到内存中
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
/**
* 密码编码器,不解析密码
*
* @return 密码解析器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
将登录页传来的用户名密码和内存中用户名密码做匹配认证。
自定义认证逻辑
在实际项目中,认证逻辑是需要自定义控制的。将 UserDetailsService
接口的实现类放入Spring容器即可自定义认证逻辑
InMemoryUserDetailsManager
就是 UserDetailsService
接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义 UserDetailsService
接口的实现类
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService
的实现类必须重写 loadUserByUsername()
方法,该方法定义了具体的认证逻辑。
参数 username
是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个 UserDetails
对象,该对象是 Spring Security 提供的用户对象,包含用户名、密码、权限。Spring Security 会根据UserDetails 对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
数据库建表
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'baizhan', 'baizhan', '13812345678');
INSERT INTO `users` VALUES (2, 'sxt', 'sxt', '13812345678');
INSERT INTO `users` VALUES (3, 'demo', '$2a$10$tBjD3Sdp6RGZdTlJoHns..3Yodf.DzahrkVHVoQGKSHCTxavCelHO', NULL);
编写实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
private String phone;
}
编写dao接口
public interface UsersMapper extends BaseMapper<Users> {
}
在 SpringBoot 启动类中添加 @MapperScan
注解,扫描Mapper文件夹
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
public class Application {
public static void main(String[] args)
{
SpringApplication.run(MysecurityApplication.class, args);
}
}
创建 UserDetailsService
的实现类,编写自定义认证逻辑
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
// 自定义认证逻辑
@Override
public UserDetails
loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.构造查询条件
QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
// 2.查询用户
Users users = usersMapper.selectOne(wrapper);
// 3.封装为UserDetails对象
UserDetails userDetails = User
.withUsername(users.getUsername())
.password(users.getPassword())
.authorities("admin")
.build();
// 4.返回封装好的UserDetails对象
return userDetails;
}
}
密码解析器PasswordEncoder
在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密码。
Spring Security要求容器中必须有 PasswordEncoder
实例,之前使用的 NoOpPasswordEncoder
是 PasswordEncoder
的实现类,意思是不解析密码,使用明文密码。
Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder
。
@SpringBootTest
public class PasswordEncoderTest {
@Test
public void testBCryptPasswordEncoder(){
//创建解析器
PasswordEncoder encoder = new BCryptPasswordEncoder();
//密码加密
String password = encoder.encode("baizhan");
System.out.println("加密后:"+password);
//密码校验
/**
* 参数1:明文密码
* 参数2:加密密码
* 返回值:是否校验成功
*/
boolean result = encoder.matches("baizhan","$2a$10$/MImcrpDO21HAP2amayhme8j2SM0YM50/WO8YBH.NC1hEGGSU9ByO");
System.out.println(result);
}
}
将 BCryptPasswordEncoder
的实例放入Spring容器即可,并且在用户注册完成后,将密码加密再保存到数据库
@Configuration
public class SecurityConfig {
/**
* 密码编码器,不解析密码
*
* @return 密码解析器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义登录界面
Spring Security也支持用户自定义登录页面
编写登录页面
在Spring Security配置类自定义登录页面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//Spring Security配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin().loginPage("/login.html")
// 自定义登录页面
.usernameParameter("username")
// 表单中的用户名项
.passwordParameter("password")
// 表单中的密码项
.loginProcessingUrl("/login")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
.successForwardUrl("/main")
//登录成功后跳转的路径
.failureForwardUrl("/fail");
//登录失败后跳转的路径
// 需要认证的资源
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
// 登录页不需要认证
.anyRequest().authenticated();
//其余所有请求都需要认证
//关闭csrf防护
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css/**");
}
}
Spring Security 为了防止CSRF攻击,默认开启了 CSRF
防护,这限制了除了GET请求以外的大多数方法。我们要想正常使用Spring
Security需要突破CSRF防护
- 解决方法一:关闭CSRF防护:
http.csrf().disable();
- 解决方法二:要求访问时携带参数名为
_csrf值
为令牌,令牌在服务端产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问
<form class="form" action="/login"method="post">
<!-- 在表单中添加令牌隐藏域 -->
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
<input type="text" placeholder="用户名" name="username">
<input type="password" placeholder="密码" name="password">
<button type="submit">登录</button>
</form>
fail.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>失败页面</title>
</head>
<body>
<h1>登录失败,请<a href="/login.html">重新登录</a></h1>
</body>
</html>
login.html
<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<link href="/css/styles.css" rel="stylesheet" >
</head>
<body>
<div class="htmleaf-container">
<div class="wrapper">
<div class="container">
<h1>Welcome</h1>
<form class="form" action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
<input type="text" placeholder="用户名" name="username">
<input type="password" placeholder="密码" name="password">
<input type="checkbox" name="remember-me" value="true"/>记住我</br>
<button type="submit" id="login-button">登录</button>
</form>
</div>
</div>
</div>
</body>
</html>
会话管理
需求场景:用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。
Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从 SecurityContext
对象中获取用户信息,SecurityContext
对象与当前线程进行绑定。
获取用户信息的写法
@RestController
public class MyController {
// 获取当前登录用户名
@RequestMapping("/users/username")
public String getUsername(){
// 1.获取会话对象
SecurityContext context = SecurityContextHolder.getContext();
// 2.获取认证对象
Authentication authentication = context.getAuthentication();
// 3.获取登录用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
}
当我们的登录成功后,去请求 /users/username
会返回登录用户的用户名
认证成功后的处理方式
登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。
自定义登录成功处理器
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 拿到登录用户的信息
UserDetails userDetails =(UserDetails)authentication.getPrincipal();
System.out.println("用户名:"+userDetails.getUsername());
System.out.println("一些操作...");
// 重定向到主页
response.sendRedirect("/main");
}
}
配置登录成功处理器
http.formLogin() // 使用表单登录
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
//.successForwardUrl("/main") //登录成功后跳转的路径
.successHandler(new MyLoginSuccessHandler()) //登录成功处理器
.failureForwardUrl("/fail"); //登录失败后跳转的路径
认证失败后的处理方式
登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数,记录日志等,可以自定义登录失败处理器。
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println("记录失败日志...");
response.sendRedirect("/fail");
}
}
配置登录失败处理器
http.formLogin() // 使用表单登录
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
//
.successForwardUrl("/main") //登录成功后跳转的路径
.successHandler(new MyLoginSuccessHandler()) //登录成功处理器
//
.failureForwardUrl("/fail") //登录失败后跳转的路径
.failureHandler(new MyLoginFailureHandler()); //登录失败处理器
// 需要认证的资源
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //登录页不需要认证
.antMatchers("/fail").permitAll() //失败页不需要认证
.anyRequest().authenticated(); //其余所有请求都需要认证
退出登录
退出登录后,Spring Security进行了以下操作:
- 清除认证状态
- 销毁HttpSession对象
- 跳转到登录页面
main.html
添加退出登录连接
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcuticon" href="resources/favicon.ico" th:href="@{/static/favicon.ico}">
<title>登录首页</title>
</head>
<body>
<h1>主页面</h1>
<a href="/logout">退出登录</a>
</body>
</html>
配置退出登录
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
.loginProcessingUrl("/login")
// 表单中的用户名项
.usernameParameter("username")
// 表单中的密码项
.passwordParameter("password")
// 登录成功处理器
.successHandler(new MyLoginSuccessHandler())
// 登录失败处理器
.failureHandler(new MyLoginFailureHandler());
// 关闭csrf防护
http.csrf().disable();
// 退出登录配置
http.logout()
// 登出路径
.logoutUrl("/logout")
// 登出成功后请求的页面
.logoutSuccessUrl("/login.html")
//清除认证状 态,默认为true
.clearAuthentication(true)
// 销毁HttpSession对象,默认为true
.invalidateHttpSession(true);
}
....
}
退出成功处理器
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("清除一些数据...");
// 重定向到登录页面
httpServletResponse.sendRedirect("/login.html");
}
}
配置退出成功处理器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
.loginProcessingUrl("/login")
// 表单中的用户名项
.usernameParameter("username")
// 表单中的密码项
.passwordParameter("password")
// 登录成功处理器
.successHandler(new MyLoginSuccessHandler())
// 登录失败处理器
.failureHandler(new MyLoginFailureHandler());
// 关闭csrf防护
http.csrf().disable();
// 退出登录配置
http.logout()
// 登出路径
.logoutUrl("/logout")
// 登出处理器
.logoutSuccessHandler(new MyLogoutSuccessHandler())
//清除认证状 态,默认为true
.clearAuthentication(true)
// 销毁HttpSession对象,默认为true
.invalidateHttpSession(true);
}
.....
}
记住我
当使用“记住我”功能登录后,Spring Security 会生成一个令牌,令牌一方面保存到数据库中,另一方面生成一个叫 remember-me 的Cookie保存到客户端。
添加记住我配置类
@Configuration
public class RememberConfig {
@Resource
private DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
// Spring Security 自带的令牌控制器
JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl = new JdbcTokenRepositoryImpl();
jdbcTokenRepositoryImpl.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
//jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
return jdbcTokenRepositoryImpl;
}
}
修改配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
.loginProcessingUrl("/login")
// 表单中的用户名项
.usernameParameter("username")
// 表单中的密码项
.passwordParameter("password")
// 登录成功处理器
.successHandler(new MyLoginSuccessHandler())
// 登录失败处理器
.failureHandler(new MyLoginFailureHandler());
// 关闭csrf防护
http.csrf().disable();
// 退出登录配置
http.logout()
// 登出路径
.logoutUrl("/logout")
// 登出成功后请求的页面
//.logoutSuccessUrl("/login.html")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
//清除认证状 态,默认为true
.clearAuthentication(true)
// 销毁HttpSession对象,默认为true
.invalidateHttpSession(true);
// 记住我配置
http.rememberMe()
// 登录逻辑交给哪个对象
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository)
// 保存时间(秒)
.tokenValiditySeconds(30);
}
.....
}
在登录页面添加“记住我复选框”
<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<link href="/css/styles.css" rel="stylesheet" >
</head>
<body>
<div class="htmleaf-container">
<div class="wrapper">
<div class="container">
<h1>Welcome</h1>
<form class="form" action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
<input type="text" placeholder="用户名" name="username">
<input type="password" placeholder="密码" name="password">
<input type="checkbox" name="remember-me" value="true"/>记住我</br>
<button type="submit" id="login-button">登录</button>
</form>
</div>
<ul class="bg-bubbles">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</body>
</html>
授权
授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。RBAC是业界普遍采用的授权方式
Role-Based Access Control
即按角色进行授权。比如在企业管理系统中,主体角色为总经理可以查询企业运营报表。
if(主体.hasRole("总经理角色")){
查询运营报表
}
这样会造成,当查询运营报表的角色发生变化时,代码也要相关的进行改变
Resource-Based Access Control
基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报表。
if(主体.hasPermission("查询报表权限")){
查询运营报表
}
授权__权限表设计
用户、角色、权限都是多对多的关系
这种方式需要指定用户有哪些权限,如:张三有查询工资的权限,即在用户权限中间表中添加一条数据,分别记录张三和查询工资权限ID。但在系统中权限数量可能非常庞大,如果一条一条添加维护数据较为繁琐。所以我们通常的做法是再加一张角色表:
用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。
编写查询权限的方法
model包
public class BaseEntity {
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.JSON_STYLE).toString();
}
}
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class Permission extends BaseEntity {
/**
* 权限id
*/
private String pid;
/**
* 权限名称
*/
private String permissionName;
/**
* 权限路径
*/
private String url;
}
@Data
public class Role {
/**
* 角色id
*/
private String rid;
/**
* 角色名称
*/
private String roleName;
/**
* 角色描述
*/
private String roleDesc;
}
@Data
public class Users {
/**
* 用户id
*/
private Integer uid;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 手机号码
*/
private String phone;
}
UsersMapper.java
public interface UsersMapper extends BaseMapper<Users> {
/**
* 根据用户名查询所有权限
*
* @param username 用户名
* @return 结果
*/
List<Permission> findPermissionAllByUsername(@Param("username") String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.doublew2w.mapper.UsersMapper">
<select id="findPermissionAllByUsername" resultType="com.doublew2w.model.Permission">
SELECT DISTINCT e.pid,
e.permissionName,
e.url
FROM users AS a
LEFT JOIN users_role AS b on a.uid = b.uid
LEFT JOIN role AS c on b.rid = c.rid
LEFT JOIN role_permission AS d on c.rid = d.rid
LEFT JOIN permission AS e on d.pid = e.pid
where username = #{username}
</select>
</mapper>
修改自认证逻辑
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Resource
private UsersMapper usersMapper;
/**
* 自定义认证逻辑
*
* @param username 用户名
* @return 用户封装对象
* @throws UsernameNotFoundException 异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.构造查询条件
QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
//2.查询结果
Users users = usersMapper.selectOne(wrapper);
if (users == null) {
return null;
}
// 3.查询用户权限
List<Permission> permissions = usersMapper.findPermissionAllByUsername(username);
// 4.将自定义权限集合转为Security的权限类型集合 List<GrantedAuthority>
List<GrantedAuthority> grantedAuthority = new ArrayList<>();
for (Permission permission : permissions) {
grantedAuthority.add(new SimpleGrantedAuthority(permission.getUrl()));
}
// 5.封装为UserDetails对象
UserDetails userDetails = User.withUsername(users.getUsername())
.password(users.getPassword())
.authorities(grantedAuthority)
.build();
// 6.返回对象
return userDetails;
}
}
配置类设置访问控制
由于没有权限,访问被拦截,就会抛出403异常。
编写控制器类,添加控制器方法资源
@RestController
public class MyController {
@GetMapping("/reportform/find")
public String findReportForm() {
return "查询报表";
}
@GetMapping("/salary/find")
public String findSalary() {
return "查询工资";
}
@GetMapping("/staff/find")
public String findStaff() {
return "查询员工";
}
}
修改Security配置类
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("/login.html").permitAll()
// 给资源配置需要的权限
.antMatchers("/reportform/find").hasAnyAuthority("/reportform/find")
.antMatchers("/salary/find").hasAnyAuthority("/salary/find")
.antMatchers("/staff/find").hasAnyAuthority("/staff/find")
//表示任何请求都需要认证后才能访问
.anyRequest().authenticated();
自定义访问控制逻辑
我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源URL的权限
自定义访问控制逻辑
@Service
public class MyAuthorizationService {
/**
* 自定义访问控制逻辑,返回值为是否可以访问资源
*
* @param request 请求
* @param authentication 权限
* @return 结果
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获取会话中的登录用户
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// 获取登录用户的权限
Collection<? extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities();
// 获取请求的URL路径
String uri = request.getRequestURI();
// 将URL路径封装为权限对象
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(uri);
// 判断用户的权限集合是否包含请求的URL权限对象
return authorities.contains(authority);
}
return false;
}
}
在配置文件中使用自定义访问控制逻辑
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("/login.html").permitAll()
// 任何请求都使用自定义访问控制逻辑
.anyRequest().access("@myAuthorizationService.hasPermission(request,authentication)");
注解设置访问控制
除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。
@Secured
注解是基于角色的权限控制,要求 UserDetails
中的权限名必须以
ROLE_
开头。
启动类
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在控制器上添加注解
@Secured("ROLE_reportform")
@GetMapping("/reportform/find")
public String findReportForm() {
return "查询报表";
}
不过该注解一般很少用,常用的是下面的注解 @PreAuthorize
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("/login.html").permitAll()
.antMatchers("/fail").permitAll()
.anyRequest().authenticated();
@PreAuthorize
注解可以在方法执行前判断用户是否具有权限
@SpringBootApplication
@MapperScan("com.doublew2w.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
public class MyController {
@PreAuthorize("hasAnyAuthority('/reportform/find')")
@GetMapping("/reportform/find")
public String findReportForm() {
return "查询报表";
}
@PreAuthorize("hasAnyAuthority('/salary/find')")
@GetMapping("/salary/find")
public String findSalary() {
return "查询工资";
}
@PreAuthorize("hasAnyAuthority('/staff/find')")
@GetMapping("/staff/find")
public String findStaff() {
return "查询员工";
}
}
在前端进行访问控制
当没有对应权限的时候,就没显示该菜单
引入依赖
<!--Spring Security整合Thymeleaf-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
修改main.html
<!doctype html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcuticon" href="resources/favicon.ico" th:href="@{/static/favicon.ico}">
<title>登录首页</title>
</head>
<body>
<h1>主页面</h1>
<ul>
<li sec:authorize="hasAnyAuthority('/reportform/find')"><a href="/reportform/find">查询报表</a></li>
<li sec:authorize="hasAnyAuthority('/salary/find')"><a href="/salary/find">查询工资</a></li>
<!-- 没有这个权限就不显示-->
<li sec:authorize="hasAnyAuthority('/staff/find')"><a href="/staff/find">查询员工</a></li>
</ul>
<a href="/logout">退出登录</a>
</body>
</html>
编写当返回403的时候,处理页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>权限不足</title>
</head>
<body>
<h1>您的权限不足,请联系管理员!</h1>
</body>
</html>
编写权限不足处理类并配置
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("/noPermission.html");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService方法
.loginProcessingUrl("/login")
// 表单中的用户名项
.usernameParameter("username")
// 表单中的密码项
.passwordParameter("password")
// 登录成功处理器
.successHandler(new MyLoginSuccessHandler())
// 登录失败处理器
.failureHandler(new MyLoginFailureHandler());
// 关闭csrf防护
http.csrf().disable();
// 退出登录配置
http.logout()
// 登出路径
.logoutUrl("/logout")
// 登出成功后请求的页面
//.logoutSuccessUrl("/login.html")
.logoutSuccessHandler(new MyLogoutSuccessHandler())
//清除认证状 态,默认为true
.clearAuthentication(true)
// 销毁HttpSession对象,默认为true
.invalidateHttpSession(true);
// 记住我配置
http.rememberMe()
// 登录逻辑交给哪个对象
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository)
// 保存时间(秒)
.tokenValiditySeconds(30);
// 权限拦截配置
http.authorizeRequests()
// 表示任何权限都可以访问
.antMatchers("/login.html").permitAll()
.antMatchers("/fail").permitAll()
.anyRequest().authenticated();
//异常处理
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
}
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css/**");
}
/**
* 密码编码器,不解析密码
*
* @return 密码解析器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}