初始情況的簡要描述:讓我們假設一個基于Spring Boot的RESTful API充當OAuth2資源服務器。該資源服務器是使用Spring Security 5和常見方法共同配置的。外部授權服務器將用戶資訊(例如E-Mail、名字、姓氏)作為JWT請求,在客戶端認證時收到。由授權服務器持有的通用用戶資訊被資源服務器特定的用戶資訊(如業務角色、域UID)所擴展。網域用戶由兩個資料源的資訊組成:
在資源服務器的資料庫中創建了從未通過JWT在資源服務器上進行過認證的新用戶。資源服務器的資料庫相應地包含了每個使用資源服務器 API 的用戶的用戶物體,其中包括由授權服務器同步的資訊和由資源服務器的業務邏輯補充的資訊。
public class User {
//從授權服務器同步的資訊
private String subject;
private String preferredUsername;
private String firstName;
private String lastName;
private String email;
//由資源服務器的業務邏輯添加的資訊。
private UUID id。
private String businessRole;
}
域用戶的同步是通過監聽AuthenticationSuccessEvent并在資料庫中創建用戶或在必要時更新它來進行的。總而言之,對于每個OAuth2用戶(由主題宣告確定),在資料庫中都有一個帶有特定領域附加資訊的領域組態檔。
這篇文章也明確描述了這種用戶資料的同步和分發。
現在來談談實際的問題:雖然OAuth2作用域控制了應用程式所擁有的授權,但也必須控制用戶擁有哪些授權。用戶的授權是針對特定領域的,并記錄在資源服務器的資料庫中。例如,一個用戶應該只能洗掉他所創建的評論。這種訪問控制不能通過OAuth2作用域來控制。作為旁證,我說的是使用@PreAuthorize或@PostAuthorize的Spring Method Security。
@GetMapping
@PreAuthorize("...")
public void func(@AuthenticationPrincipal JWT jwt){
}
例如,可以通過基于屬性的訪問控制(ABAC)來控制這種訪問。然而,這假定當前的@AuthenticationPrincipal不是JWT,因為它是Spring資源服務器的標準,而是域特定的User組態檔的一個實體。是否有辦法將JWT轉換為User組態檔,可能使用UserDetailsService?
對于Spring Security 5 OAuth2資源服務器來說,是否有一種推薦的方法來根據JWT從資料庫中加載資訊,更確切地說,是加載User?
uj5u.com熱心網友回復:
我通常最終使用的是一個JWT轉換器,并創建一個自定義會話物件(AbstractAuthenticationToken)
@Autowired
private final UserService userService。
@Override
public void configure(HttpSecurity http) {
http
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(new JwtConverter(userService))
... 其他安全配置 ...
JWT轉換器:
public class JwtConverter implements Converter< Jwt, AbstractAuthenticationToken> {
private final UserService userService。
public JwtConverter(UserService userService) {
this.userManager = userService。
}
@Override
public AbstractAuthenticationToken convert(@NotNull final Jwt jwt){
//這里是我通常懶得創建或同步用戶的地方,而不是AuthenticationSuccessEvent。
User user = userService.getByEmail(jwt.getClaim("email") )。)
收藏<? extends GrantedAuthority> authorities = translateAuthorities(jwt)。
return new CustomSession(user, jwt, authorities)。
}
//從你的jwt中翻譯出合適的內容(我使用的是角色要求)。
private static Collection< ? extends GrantedAuthority> translateAuthorities(finalJwt jwt) {
Collection<String> userRoles = jwt.getClaimAsStringList("role")。
if (userRoles != null)
return userRoles
.stream()
.map(role -> new SimpleGrantedAuthority("ROLLE_" role.toUpperCase())
.收集(Collectors.toSet())。
return Collections.emptySet()。
}
粗略的例子CustomSession:
public class CustomSession extends AbstractAuthenticationToken {
final private User user。
final private Jwt jwt;
public CustomSession(User user, Jwt jwt, Collection< ? extends GrantedAuthority> authorities) {
super(authorities)。
this.user = user;
this.jwt = jwt;
this.setAuthenticated(true)。
}
@Override.
public Object getPrincipal() {
return getUser()。
}
@Override
public Object getCredentials() {
return getJwt()。
}
public Jwt getJwt() {
return jwt;
}
public User getUser() {
return 用戶。
}
//*********************************************************************
//* 靜態幫助器
//*********************************************************************
public static Optional<CustomSession> GetSession() {
return Optional.ofNullable(SecurityContextHolder.getContext() .getAuthentication()
.map(s -> s instanceof CustomSession ? (CustomSession) s : null)。)
}
public static Optional<Jwt> GetJwt(){
return GetSession().map(CustomSession::getJwt)。
}
public static Optional<User> GetUser() {
return GetSession().map(CustomSession::getUser)。
}
public static Optional<String> GetUserId() {
return GetUser().map(User::getId)。
}
public static Optional<String> GetUserEmail() {
return GetUser().map(User::getEmail)。
}
在我的案例中,我在userService.getByEmail()后面使用JPA,所以需要注意的是User物件是 "分離的",所以你不應該使用這個物件進行用戶更新。
另一個選擇是擴展BearerTokenAuthenticationToken并提供一個建構式來設定權限(而不是CustomSession)。
====
吃完午飯回來,完全忘記了提及權限檢查。我介紹了一個自定義權限評估器(一個簡單的例子讓你開始):
public class CustomPermissionEvaluator implements PermissionEvaluator {
private SecurityService securityService。
public CustomPermissionEvaluator(SecurityService securityService) {
this.securityService = securityService。
}
@Override
public boolean hasPermission(Authentication auth, Object resourceId, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| !(action instanceof String) || StringUtils.isBlank((String) action) ){
return false。
}
try {
return securityService.hasAccess((String) resourceId, (String) action) 。
} catch(Exception e){
return false;
}
}
@Override; }
public boolean hasPermission(Authentication auth, Serializable resourceId, String component, Object action) {
if ((auth == null)
|| !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
|| StringUtils.isBlank((String) resourceId)
|| !(action instanceof String) || StringUtils.isBlank((String) action) ) {
return false。
}
try {
return securityService.hasAccess((String) resourceId, component, (String) action) 。
} catch(Exception e){
return false。
}
}
}
通過:
配置@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired SecurityService securityService。
@Override[/span
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(securityService)>)。
return expressionHandler。
}
然后利用@PreAuthorize("hasPersmission(..)")注解,如:
// Can I Create {SubDomain} on resourceId
@PreAuthorize("hasPermission(#resourceId, 'SubDomain', 'CREATE')")
void example1(int resourceId) {...}
//Can I Update resource
@PreAuthorize("hasPermission(#resourceId, 'UPDATE')")
可能不完全是你要找的,但另一個(在我看來是干凈的)解決方案。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/329360.html
標籤:
上一篇:繼承和訪問屬性
