別再讓你的微服務裸奔了,基於 Spring Session & Spring Security 微服務權限控制
- 2019 年 10 月 30 日
- 筆記
微服務架構
- 網關:路由用戶請求到指定服務,轉發前端 Cookie 中包含的 Session 信息;
- 用戶服務:用戶登錄認證(Authentication),用戶授權(Authority),用戶管理(Redis Session Management)
- 其他服務:依賴 Redis 中用戶信息進行接口請求驗證
用戶 – 角色 – 權限表結構設計
- 權限表
權限表最小粒度的控制單個功能,例如用戶管理、資源管理,表結構示例:
id | authority | description |
---|---|---|
1 | ROLE_ADMIN_USER | 管理所有用戶 |
2 | ROLE_ADMIN_RESOURCE | 管理所有資源 |
3 | ROLE_A_1 | 訪問 ServiceA 的某接口的權限 |
4 | ROLE_A_2 | 訪問 ServiceA 的另一個接口的權限 |
5 | ROLE_B_1 | 訪問 ServiceB 的某接口的權限 |
6 | ROLE_B_2 | 訪問 ServiceB 的另一個接口的權限 |
- 角色 – 權限表
自定義角色,組合各種權限,例如超級管理員擁有所有權限,表結構示例:
id | name | authority_ids |
---|---|---|
1 | 超級管理員 | 1,2,3,4,5,6 |
2 | 管理員A | 3,4 |
3 | 管理員B | 5,6 |
4 | 普通用戶 | NULL |
- 用戶 – 角色表
用戶綁定一個或多個角色,即分配各種權限,示例表結構:
user_id | role_id |
---|---|
1 | 1 |
1 | 4 |
2 | 2 |
用戶服務設計
Maven 依賴(所有服務)
<!-- Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Session Redis --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
應用配置 application.yml
示例:
# Spring Session 配置 spring.session.store-type=redis server.servlet.session.persistent=true server.servlet.session.timeout=7d server.servlet.session.cookie.max-age=7d # Redis 配置 spring.redis.host=<redis-host> spring.redis.port=6379 # MySQL 配置 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://<mysql-host>:3306/test spring.datasource.username=<username> spring.datasource.password=<passowrd>
用戶登錄認證(authentication)與授權(authority)
Slf4j public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final UserService userService; CustomAuthenticationFilter(String defaultFilterProcessesUrl, UserService userService) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl, HttpMethod.POST.name())); this.userService = userService; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { JSONObject requestBody = getRequestBody(request); String username = requestBody.getString("username"); String password = requestBody.getString("password"); UserDO user = userService.getByUsername(username); if (user != null && validateUsernameAndPassword(username, password, user)){ // 查詢用戶的 authority List<SimpleGrantedAuthority> userAuthorities = userService.getSimpleGrantedAuthority(user.getId()); return new UsernamePasswordAuthenticationToken(user.getId(), null, userAuthorities); } throw new AuthenticationServiceException("登錄失敗"); } /** * 獲取請求體 */ private JSONObject getRequestBody(HttpServletRequest request) throws AuthenticationException{ try { StringBuilder stringBuilder = new StringBuilder(); InputStream inputStream = request.getInputStream(); byte[] bs = new byte[StreamUtils.BUFFER_SIZE]; int len; while ((len = inputStream.read(bs)) != -1) { stringBuilder.append(new String(bs, 0, len)); } return JSON.parseObject(stringBuilder.toString()); } catch (IOException e) { log.error("get request body error."); } throw new AuthenticationServiceException(HttpRequestStatusEnum.INVALID_REQUEST.getMessage()); } /** * 校驗用戶名和密碼 */ private boolean validateUsernameAndPassword(String username, String password, UserDO user) throws AuthenticationException { return username == user.getUsername() && password == user.getPassword(); } } @EnableWebSecurity @AllArgsConstructor public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String LOGIN_URL = "/user/login"; private static final String LOGOUT_URL = "/user/logout"; private final UserService userService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(LOGIN_URL).permitAll() .anyRequest().authenticated() .and() .logout().logoutUrl(LOGOUT_URL).clearAuthentication(true).permitAll() .and() .csrf().disable(); http.addFilterAt(bipAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .rememberMe().alwaysRemember(true); } /** * 自定義認證過濾器 */ private CustomAuthenticationFilter customAuthenticationFilter() { CustomAuthenticationFilter authenticationFilter = new CustomAuthenticationFilter(LOGIN_URL, userService); return authenticationFilter; } }
其他服務設計
應用配置 application.yml
示例:
# Spring Session 配置 spring.session.store-type=redis # Redis 配置 spring.redis.host=<redis-host> spring.redis.port=6379
全局安全配置
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable(); } }
用戶認證信息獲取
用戶通過用戶服務登錄成功後,用戶信息會被緩存到 Redis,緩存的信息與 CustomAuthenticationFilter
中 attemptAuthentication()
方法返回的對象有關,如上所以,返回的對象是 new UsernamePasswordAuthenticationToken(user.getId(), null, userAuthorities)
,即 Redis 緩存了用戶的 ID 和用戶的權力(authorities)。
UsernamePasswordAuthenticationToken
構造函數的第一個參數是 Object 對象,所以可以自定義緩存對象。
在微服務各個模塊獲取用戶的這些信息的方法如下:
@GetMapping() public WebResponse test(@AuthenticationPrincipal UsernamePasswordAuthenticationToken authenticationToken){ // 略 }
權限控制
- 啟用基於方法的權限註解
@SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
- 簡單權限校驗
例如,刪除角色的接口,僅允許擁有ROLE_ADMIN_USER
權限的用戶訪問。
/** * 刪除角色 */ @PostMapping("/delete") @PreAuthorize("hasRole('ADMIN_USER')") public WebResponse deleteRole(@RequestBody RoleBean roleBean){ // 略 }
@PreAuthorize("hasRole('<authority>')")
可作用於微服務中的各個模塊
- 自定義權限校驗
如上所示,hasRole()
方法是 Spring Security 內嵌的,如需自定義,可以使用 Expression-Based Access Control,示例:
/** * 自定義校驗服務 */ @Service public class CustomService{ public boolean check(UsernamePasswordAuthenticationToken authenticationToken, String extraParam){ // 略 } } /** * 刪除角色 */ @PostMapping() @PreAuthorize("@customService.check(authentication, #userBean.username)") public WebResponse custom(@RequestBody UserBean userBean){ // 略 }
authentication
屬於內置對象,#
獲取入參的值
- 任意用戶權限動態修改
原理上,用戶的權限信息保存在 Redis 中,修改用戶權限就需要操作 Redis,示例:
@Service @AllArgsConstructor public class HttpSessionService<S extends Session> { private final FindByIndexNameSessionRepository<S> sessionRepository; /** * 重置用戶權限 */ public void resetAuthorities(Long userId, List<GrantedAuthority> authorities){ UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(userId, null, authorities); Map<String, S> redisSessionMap = sessionRepository.findByPrincipalName(String.valueOf(userId)); redisSessionMap.values().forEach(session -> { SecurityContextImpl securityContext = session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); securityContext.setAuthentication(newToken); session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext); sessionRepository.save(session); }); } }
修改用戶權限,僅需調用 httpSessionService.resetAuthorities()
方法即可,實時生效。
© 著作權歸作者所有,轉載或內容合作請聯繫作者
● APM工具尋找了一圈,發現SkyWalking才是我的真愛
● Spring Boot 注入外部配置到應用內部的靜態變量
● Java 使用 UnixSocket 調用 Docker API
● Service Mesh – gRPC 本地聯調遠程服務
● Spring Security 實戰乾貨:如何保護用戶密碼
● Spring Boot RabbitMQ – 優先級隊列
原文鏈接:https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=2247486167&idx=2&sn=76dba01d16b7147c9b1dfb7cbf2d8d28&chksm=fb3f132ccc489a3ad2ea05314823d660c40e8af90dcd35800422899958f98b4a258d23badba8&token=280305379&lang=zh_CN#rd
本文由博客一文多發平台 OpenWrite 發佈!