SpringSecurity-SpirngBoot-方法级授权(SpringSecurity6.3新特性)(四)
本章使用SpringSecurity6.3新特性实现数据级别的鉴权,主要的目的是实现不同权限的用户查询同一个方法,限制一些内容只能拥有特定权限的用户才能看到,其他没有该权限的用户显示为空。
在上一节的基础上,新建spring-security-authorization-data分支。
-
修改SecurityConfiguration类,添加rob用户,权限为"message:read", “user:read”;新建luke用户,权限为"message:read"
@Bean CustomUserRepository customUserRepository() {String password = new BCryptPasswordEncoder().encode("password");CustomUser customUser1 = new CustomUser(1L, "rob", password, "message:read", "user:read");CustomUser customUser2 = new CustomUser(2L, "luke", password, "message:read");Map<String, CustomUser> emailToCustomUser = new HashMap<>();emailToCustomUser.put(customUser1.getEmail(), customUser1);emailToCustomUser.put(customUser2.getEmail(), customUser2);return new MapCustomUserRepository(emailToCustomUser); }
修改CustomUser、CustomUserRepositoryUserDetailsService类,以适配修改后的SecurityConfiguration:
public class CustomUser {private final long id;private final String email;@JsonIgnoreprivate final String password;// 用户权限private final String[] authoritie;@JsonCreatorpublic CustomUser(long id, String email, String password, String ...authoritie) {this.id = id;this.email = email;this.password = password;this.authoritie = authoritie;}public long getId() {return this.id;}public String getEmail() {return this.email;}public String getPassword() {return this.password;}public String[] getAuthoritie() {return authoritie;}@Overridepublic String toString() {return email;}@Overridepublic int hashCode() {return email.hashCode();}@Overridepublic boolean equals(Object obj) {return this.toString().equals(obj.toString());} }
@Service public class CustomUserRepositoryUserDetailsService implements UserDetailsService {private final CustomUserRepository userRepository;public CustomUserRepositoryUserDetailsService(CustomUserRepository userRepository) {this.userRepository = userRepository;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询用户名对应的用户CustomUser customUser = this.userRepository.findCustomUserByEmail(username);if (customUser == null) {// 用户不存在 抛出异常throw new UsernameNotFoundException("username " + username + " is not found");}return new CustomUserDetails(customUser);}static final class CustomUserDetails extends CustomUser implements UserDetails {private final List<GrantedAuthority> ROLE_USER;CustomUserDetails(CustomUser customUser) {super(customUser.getId(), customUser.getEmail(), customUser.getPassword(), customUser.getAuthoritie());ROLE_USER = Collections.unmodifiableList(AuthorityUtils.createAuthorityList(customUser.getAuthoritie()));}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return ROLE_USER;}@Overridepublic String getUsername() {return getEmail();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}} }
-
因为使用的是SpringSecurity6.3,把pom文件的SpringBoot版本修改为3.3.1以使用SpringSecurity6.3。导入h2和spring-boot-starter-data-jpa包实现简单的数据库查询
<?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>3.3.1</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.jackmouse</groupId><artifactId>jackmouse-spring-boot-security-hello</artifactId><version>0.0.1-SNAPSHOT</version><name>jackmouse-spring-boot-security-hello</name><description>jackmouse-spring-boot-security-hello</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><!--security依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-data</artifactId></dependency><!--spring-boot Web支持--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</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-test</artifactId><scope>test</scope></dependency><!--security测试模块--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency><!-- Selenium Web驱动 --><dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>htmlunit-driver</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
-
创建MessageController类和MessageRepository接口,实现对message的查询
@RestController public class MessageController {private final MessageRepository messages;public MessageController(MessageRepository messages) {this.messages = messages;}@GetMapping("/message")List<Message> getMessages() {List<Message> all = this.messages.findAll();return all;}@GetMapping("/message/{id}")Optional<Message> getMessages(@PathVariable Long id) {return this.messages.findById(id);} }
@Repository @AuthorizeReturnObject public interface MessageRepository extends CrudRepository<Message, Long> {@Query("select m from Message m where m.to.id = ?#{ authentication.name }")List<Message> findAll(); }
-
实体类创建
package com.jackmouse.security.entity;import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.jackmouse.security.annotation.AuthorizeRead; import jakarta.persistence.*; import org.springframework.security.authorization.method.AuthorizeReturnObject;import java.time.Instant;@Entity @JsonSerialize(as = Message.class) @AuthorizeReturnObject public class Message {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;private String text;private String summary;private Instant created = Instant.now();@ManyToOneprivate User to;public User getTo() {return this.to;}public void setTo(User to) {this.to = to;}public Long getId() {return this.id;}public void setId(Long id) {this.id = id;}public Instant getCreated() {return this.created;}public void setCreated(Instant created) {this.created = created;}@AuthorizeRead("message")public String getText() {return this.text;}public void setText(String text) {this.text = text;}@AuthorizeRead("message")public String getSummary() {return this.summary;}public void setSummary(String summary) {this.summary = summary;}}
@Entity(name = "users") @JsonSerialize(as = User.class, contentUsing = JsonSerializer.class) public class User {@Idprivate String id;private String firstName;private String lastName;private String email;private String password;public String getId() {return this.id;}public void setId(String id) {this.id = id;}@AuthorizeRead("user")public String getFirstName() {return this.firstName;}public void setFirstName(String firstName) {this.firstName = firstName;}@AuthorizeRead("user")public String getLastName() {return this.lastName;}public void setLastName(String lastName) {this.lastName = lastName;}public String getEmail() {return this.email;}public void setEmail(String email) {this.email = email;}public String getPassword() {return this.password;}public void setPassword(String password) {this.password = password;} }
这里的@AuthorizeRead注解后面会介绍到
-
创建AuthorizeRead注解,这里使用到SpringSecurity官方文档介绍到的模版注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasAuthority('{value}:read')") @HandleAuthorizationDenied(handlerClass = Null.class) public @interface AuthorizeRead {String value(); }
@AuthorizeRead(“user”)对应的注解是@PreAuthorize(“hasAuthority(‘user:read’)”)
@AuthorizeRead(“message”)对应的注解是@PreAuthorize(“hasAuthority(‘message:read’)”)
意味着只有拥有user:read权限,才能访问@AuthorizeRead(“user”)标记的方法,只有拥有message:read才能访问@AuthorizeRead(“message”)标记的方法。
根据官方文档的介绍,使用模版注解还必须向Spring容器中注册一个PrePostTemplateDefaults bean,以解析我们设置的模版变量
在SecurityConfiguration类中添加:
@Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static PrePostTemplateDefaults templateDefaults() {return new PrePostTemplateDefaults(); }
@AuthorizeReturnObject注解在类上,表示这个类所有的字段都需要进行鉴权。这意味着 Spring Security 将尝试代理任何返回对象,包括 String、 Integer 和其他类型。
如果您希望对方法返回值类型(如 int、 String、 Double 或这些类型的集合)的类或接口使用
@AuthorizeReturnObject
,那么您还应该发布适当的 AuthorizationAdvisorProxyFactory。在SecurityConfiguration类中添加:
@Bean static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {return (factory) -> factory.setTargetVisitor(AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes()); }
-
创建Null类实现MethodAuthorizationDeniedHandler接口。由于SpringSecurity在鉴权失败会抛出异常,我们只是希望没有权限的值不被用户看到,而不是程序报错,所以创建Null类在鉴权失败后返回null值。
@Component public class Null implements MethodAuthorizationDeniedHandler {@Overridepublic Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {return null;} }
在AuthorizeRead注解上有一个注解@HandleAuthorizationDenied(handlerClass = Null.class),表示在鉴权失败时,使用Null处理。
-
数据源准备,在resources目录下创建import.sql,内容如下:
insert into users (id,email,password,first_name,last_name) values ('rob','rob@example.com','password','Rob','Winch'); insert into users (id,email,password,first_name,last_name) values ('luke','luke@example.com','password','Luke','Taylor');insert into message (id,created,to_id,summary,text) values (100,'2014-07-10 10:00:00','rob','Hello Rob','This message is for Rob'); insert into message (id,created,to_id,summary,text) values (101,'2014-07-10 14:00:00','rob','How are you Rob?','This message is for Rob'); insert into message (id,created,to_id,summary,text) values (102,'2014-07-11 22:00:00','rob','Is this secure?','This message is for Rob');insert into message (id,created,to_id,summary,text) values (110,'2014-07-12 10:00:00','luke','Hello Luke','This message is for Luke'); insert into message (id,created,to_id,summary,text) values (111,'2014-07-12 10:00:00','luke','Greetings Luke','This message is for Luke'); insert into message (id,created,to_id,summary,text) values (112,'2014-07-12 10:00:00','luke','Is this secure?','This message is for Luke');
由于添加了h2依赖,SpringBoot会在启动时创建一个内存数据库,并插入以上数据。到这里数据级的鉴权就开发完了。
浏览器测试
登录rob用户:访问/message接口
由于rob有"message:read", "user:read"权限,所有可以看到message对象的text和summary内容、 user对象的firstName和lastName。
登录luke用户:访问/message接口
因为luke只有"message:read"权限,所以看不到user对象的firstName和lastName。