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; @JsonIgnore private final String password; // 用户权限 private final String[] authoritie; @JsonCreator public 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; } @Override public String toString() { return email; } @Override public int hashCode() { return email.hashCode(); } @Override public 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; } @Override public 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())); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return ROLE_USER; } @Override public String getUsername() { return getEmail(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public 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(); @ManyToOne private 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 { @Id private 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 { @Override public 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。