重点记忆:
1.spring Security的权限配置是在user类的getAuthorities方法中实现的
2.权限注解@EnableGlobalMethodSecurity(prePostEnabled=true)
/开启权限注解 //prePostEnabled = true 属性表示开启 Spring Security 中的 @PreAuthorize、@PostAuthorize、@PreFilter 以及 @PostFilter 四个注解
权限表:用户和管理员权限
用户表
角色表:管理员和用户
用户和角色中间表
角色和权限中间表
权限表
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.qfedu</groupId>
<artifactId>security06</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security06</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml
src/main/resources
org.springframework.boot
spring-boot-maven-plugin
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.url=jdbc:mysql:///day07db?serverTimezone=Asia/Shanghai
package com.huang.demo.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* 定义用户对象,需要实现 UserDetails 接口,对于 Spring Security 框架而言,所有的用户对象都是一个 UserDetails 的实例
*
* 如实实现接口中的方法就可以了
*/
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Set<String> permissions;
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
/**
* 这个方法用来返回当前用户的角色/权限信息
*
* 在 Spring Security 中,无论是用户角色,还是用户权限,都是从这个方法返回的
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String permission : permissions) {
authorities.add(new SimpleGrantedAuthority(permission));
}
return authorities;
}
/**
* 获取用户密码
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 账户是否没有过期
*
* 正常来说,数据库中应该也有一个描述账户是否过期的字段
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否没有被锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 密码是否没有过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
}
这个方法用来返回当前用户的角色或者权限信息,跟shiro不同的是,spring security分的很明白,能不能访问接口,终究是根据权限来进行判定,而角色只是业务层面的一种分类,而权限是底层获取的判断的值
/**
* 这个方法用来返回当前用户的角色/权限信息
*
* 在 Spring Security 中,无论是用户角色,还是用户权限,都是从这个方法返回的
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String permission : permissions) {
authorities.add(new SimpleGrantedAuthority(permission));
}
return authorities;
}
package com.huang.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huang.demo.model.RespBean;
import com.huang.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.SecurityFilterChain;
import java.io.PrintWriter;
@Configuration
public class SecurityConfig {
/**
* admin 继承 user,user 能做的事情,admin 都能做
* @return
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("admin > user");
return roleHierarchy;
}
/**
* 自己手动配置安全过滤器链
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//开始认证
http.authorizeRequests()
//具备 user 权限则可以访问 /user/** 格式的路径
.antMatchers("/user/**").hasAuthority("user")
.antMatchers("/admin/**").hasAuthority("admin")
//所有的请求,类似于 shiro 中的 /**
.anyRequest()
//必须要认证之后才能访问,类似于 shiro 中的 authc
.authenticated()
.and()
//开始配置登录表单
.formLogin()
//配置处理登录请求的接口,本质上其实就是配置过滤器的拦截规则,将来的登录请求就会在过滤器中被处理
.loginProcessingUrl("/doLogin")
//配置登录表单中用户名的 key
.usernameParameter("username")
//配置登录表单中用户密码
.passwordParameter("password")
//登录成功处理器
//req:当前请求对象
//resp:当前响应对象
//auth:当前认证成功的用户信息
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
User principal = (User) auth.getPrincipal();
principal.setPassword(null);
RespBean respBean = RespBean.ok("登录成功", principal);
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
})
//登录失败的回调
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
//登录失败可能会有多种原因
RespBean respBean = RespBean.error("登录失败");
if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,登录失败");
} else if (e instanceof UsernameNotFoundException) {
//默认情况下,这个分支是不会进来的,Spring Security 自动隐藏了了这个异常,如果系统中发生了 UsernameNotFoundException 会被自动转为 BadCredentialsException 异常然后抛出来
} else if (e instanceof LockedException) {
//如果 com.qfedu.security02.model.User.isAccountNonLocked 方法返回 false,就会进入到这里来
respBean.setMsg("账户被锁定,登录失败");
} else if (e instanceof AccountExpiredException) {
//com.qfedu.security02.model.User.isAccountNonExpired
respBean.setMsg("账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,登录失败");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,登录失败");
}
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(respBean);
PrintWriter out = resp.getWriter();
out.write(s);
})
.and()
//关闭 csrf 防御机制,这个 disable 方法本质上就是从 Spring Security 的过滤器链上移除掉 csrf 过滤器
.csrf().disable()
.exceptionHandling()
//如果用户未登录就访问某一个页面,就会触发当前方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error("尚未登录,请登录");
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
});
return http.build();
}
}
关键配置
/**
* admin 继承 user,user 能做的事情,admin 都能做
* @return
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("admin > user");
return roleHierarchy;
}
测试zhangsan用户,是admin,继承了user的所有接口
测试 用户lisi ,是user,自然只能访问user的所有接口
第一步 把SecurityConfig里面的角色继承方法roleHierarchy给注释掉,
第二步骤权限访问路径注释掉
第三步加上权限注解@EnableGlobalMethodSecurity(prePostEnabled=true)
/开启权限注解
//prePostEnabled = true 属性表示开启 Spring Security 中的 @PreAuthorize、@PostAuthorize、@PreFilter 以及 @PostFilter 四个注解
即SecurityConfig写成
package com.huang.demo.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huang.demo.model.RespBean;
import com.huang.demo.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.SecurityFilterChain;
import java.io.PrintWriter;
@Configuration
//开启权限注解
//prePostEnabled = true 属性表示开启 Spring Security 中的 @PreAuthorize、@PostAuthorize、@PreFilter 以及 @PostFilter 四个注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
/* *//**
* admin 继承 user,user 能做的事情,admin 都能做
* @return
*//*
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("admin > user");
return roleHierarchy;
}*/
/**
* 自己手动配置安全过滤器链
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//开始认证
http.authorizeRequests()
/* //具备 user 权限则可以访问 /user/** 格式的路径
.antMatchers("/user/**").hasAuthority("user")
.antMatchers("/admin/**").hasAuthority("admin")*/
//所有的请求,类似于 shiro 中的 /**
.anyRequest()
//必须要认证之后才能访问,类似于 shiro 中的 authc
.authenticated()
.and()
//开始配置登录表单
.formLogin()
//配置处理登录请求的接口,本质上其实就是配置过滤器的拦截规则,将来的登录请求就会在过滤器中被处理
.loginProcessingUrl("/doLogin")
//配置登录表单中用户名的 key
.usernameParameter("username")
//配置登录表单中用户密码
.passwordParameter("password")
//登录成功处理器
//req:当前请求对象
//resp:当前响应对象
//auth:当前认证成功的用户信息
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
User principal = (User) auth.getPrincipal();
principal.setPassword(null);
RespBean respBean = RespBean.ok("登录成功", principal);
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
})
//登录失败的回调
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
//登录失败可能会有多种原因
RespBean respBean = RespBean.error("登录失败");
if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,登录失败");
} else if (e instanceof UsernameNotFoundException) {
//默认情况下,这个分支是不会进来的,Spring Security 自动隐藏了了这个异常,如果系统中发生了 UsernameNotFoundException 会被自动转为 BadCredentialsException 异常然后抛出来
} else if (e instanceof LockedException) {
//如果 com.qfedu.security02.model.User.isAccountNonLocked 方法返回 false,就会进入到这里来
respBean.setMsg("账户被锁定,登录失败");
} else if (e instanceof AccountExpiredException) {
//com.qfedu.security02.model.User.isAccountNonExpired
respBean.setMsg("账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,登录失败");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,登录失败");
}
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(respBean);
PrintWriter out = resp.getWriter();
out.write(s);
})
.and()
//关闭 csrf 防御机制,这个 disable 方法本质上就是从 Spring Security 的过滤器链上移除掉 csrf 过滤器
.csrf().disable()
.exceptionHandling()
//如果用户未登录就访问某一个页面,就会触发当前方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error("尚未登录,请登录");
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
});
return http.build();
}
}
配置这个工具类是为了把User里面获取用户权限的方法getAuthorities给抽离出来。
package com.huang.demo.config;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.io.Serializable;
import java.util.Collection;
//处理了获取权限的问题
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
//获取当前用户具备的权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (pathMatcher.match(authority.getAuthority(), (String) permission)) {
//说明当前登录的用户具备访问该接口所需要的权限
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
hasPermission为重点方法,传入三个参数
1.authentication,当前用户角色
2.不重要
3.permission当前用户所需要的权限
方便我们接下来细分权限。
UserController
package com.huang.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
/**
* 当前用户具备 user 角色的时候,就可以访问 user 接口
* @return
*/
@GetMapping("/user/hello")
//判断用户具备 user 权限才能访问
// @PreAuthorize("hasAuthority('user')")
// @PreAuthorize("hasPermission(#principal,'user:select')")
@PreAuthorize("hasPermission(#principal,'user:select')")
public String hello() {
return "hello user";
}
/**
* 当前用户具备 user 角色的时候,就可以访问 user 接口
* @return
*/
@DeleteMapping("/user/delete")
//判断用户具备 user 权限才能访问
// @PreAuthorize("hasAuthority('user')")
// @PreAuthorize("hasPermission(#principal,'user:select')")
@PreAuthorize("hasPermission(#principal,'user:delete')")
public String delete() {
return "删除成功";
}
/**
* 当前用户具备 admin 角色的时候,就可以访问 admin 接口
* @return
*/
@GetMapping("/admin/hello")
@PreAuthorize("hasPermission(#principal,'admin')")
public String admin() {
return "hello admin";
}
}
@PreAuthorize("hasPermission(#principal,'user:select')")
一样可以实现,过几天加测
用户查询成功
用户查询失败,报错403说明没有该权限,即我们的权限注解起到了作用。让user用户lisi只能做他有的权限
同理,user用户lisi没办法登录管理员的接口
zhangsan用户具有管理员的所有权限
可以进行管理员查询
但是依旧没有办法做用户查询
四.