微服务架构
- 网关:路由用户请求到指定服务,转发前端 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 依赖(所有服务)
org.springframework.boot
spring-boot-starter-security
org.springframework.session
spring-session-data-redis
应用配置 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=
spring.redis.port=6379
# MySQL 配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://:3306/test
spring.datasource.username=
spring.datasource.password=
用户登录认证(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 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=
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('
可作用于微服务中的各个模块')")
- 自定义权限校验
如上所示,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 {
private final FindByIndexNameSessionRepository sessionRepository;
/**
* 重置用户权限
*/
public void resetAuthorities(Long userId, List authorities){
UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(userId, null, authorities);
Map 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()
方法即可,实时生效。