身份的认证服务我们在网关中配置了,授权应该放在各个微服务,为此我们需要在微服务中添加依赖并配置Spring Security。

如何实现授权?业界通常基于RBAC实现授权。

什么是RBAC

RBAC分为两种方式:

基于角色的访问控制(Role-Based Access Control)

基于资源的访问控制(Resource-Based Access Control)

其中基于角色控制访问的逻辑如下:

if (主体.hasRole("总经理角色")) {
查询工资
}

如果能够查询工资的角色变为总经理和部门经理,此时需要修改逻辑为如下:

if (主体.hasRole("总经理角色") || 主体.hasRole("部门经理角色")) {
查询工资
}

当需求变动时,代码也需要修改,扩展性较差。

其中基于资源控制访问的逻辑如下:

if (主体.hasPermission("查询工资权限")) {
查询工资
}

我们只需要将该权限赋值给需要的角色即可,业务变动时,只需要授予角色相应权限即可。

数据模型设计

查询权限信息

使用下面SQL语句可以查询登陆用户的权限信息:

SELECT code FROM xc_menu WHERE id IN(
SELECT menu_id FROM xc_permission WHERE role_id IN(
SELECT role_id FROM xc_user_role WHERE user_id = '52'
)
)

找到 XcMenuMapper,定义查询权限列表的方法:

public interface XcMenuMapper extends BaseMapper<XcMenu> {

@Select("SELECT code FROM xc_menu WHERE id IN(SELECT menu_id FROM xc_permission WHERE role_id IN(SELECT role_id FROM xc_user_role WHERE user_id = #{userId}))")
List<String> selectPermissionCodeByUserId(@Param("userId") String userId);
}

同样在 XcMenuService 中定义查询方法

public interface XcMenuService extends IService<XcMenu> {
/**
* 根据用户ID查询用户权限列表
*
* @param userId 用户ID
* @return List<com.swx.ucenter.model.po.XcMenu> 权限列表
*/
List<String> selectPermissionCodeByUserId(String userId);
}

在 XcMenuServiceImpl 中实现查询方法

XcMenuServiceImpl
@Service
public class XcMenuServiceImpl extends ServiceImpl<XcMenuMapper, XcMenu> implements XcMenuService {

/**
* 根据用户ID查询用户权限列表
*
* @param userId 用户ID
* @return List<com.swx.ucenter.model.po.XcMenu> 权限列表
*/
@Override
public List<String> selectPermissionCodeByUserId(String userId) {
return baseMapper.selectPermissionCodeByUserId(userId);
}
}

修改 UserDetailsServiceImpl,增加查询列表的方法

XcMenuServiceImpl
/**
* 查询用户信息
* @param xcUserExt 用户信息
* @return UserDetails
*/
private UserDetails getUserPrincipal(XcUserExt xcUserExt) {
String password = xcUserExt.getPassword();
// 根据用户ID查询权限信息
String[] authorities = {};
List<String> permissions = xcMenuService.selectPermissionCodeByUserId(xcUserExt.getId());
if (!CollectionUtils.isEmpty(permissions)) {
// 转数组
authorities = permissions.toArray(new String[0]);
}
// 转JSON
xcUserExt.setPassword(null);
String userJson = JSON.toJSONString(xcUserExt);

// 封装 UserDetails 对象
return User.withUsername(userJson)
.password(password)
.authorities(authorities).build();
}

集成Spring Security

首先添加Spring Security的依赖信息

pom.xml
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

接着在config包下创建两个配置文件:

令牌配置,这里需要和learning-onlie-auth认证工程的令牌配置保持一致:

TokenConfig
@Configuration
public class TokenConfig {

private final static String SIGNING_KEY = "sw-code";

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}

// 令牌管理服务
@Bean(name = "authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setSupportRefreshToken(true); // 支持刷新令牌
services.setTokenStore(tokenStore()); // 令牌存储策略

TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter()));
services.setTokenEnhancer(tokenEnhancerChain);

services.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
services.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期2天
return services;
}
}

资源过滤配置

认证已经在网关配置,这里直接放行所有接口,即不用进行认证。

ResourceServerConfig
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

// 资源服务标识
public static final String RESOURCE_ID = "learning-online";

private final TokenStore tokenStore;

public ResourceServerConfig(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll();
}
}

资源服务授权

learning-online-content-api工程中集成Spring Security后,只需要在Controller的接口上添加如下注解,即可实现权限验证:

@ApiOperation("课程查询接口")
@PreAuthorize("hasAuthority('xc_teachmanager_course_list')") // 指定权限标识符
@PostMapping("/list")
public PageResult<CourseBase> list(PageParam pageParam, @RequestBody(required = false) QueryCourseParamsDTO dto) {
return courseBaseService.queryCourseBaseList(pageParam, dto);
}

细粒度控制

在查询所有课程时,我们希望当前机构的用户只能查看本机构的课程数据,这个时候需要自己来控制细粒度的过滤,在查询时从 SecurityContextHolder 上下文中拿到用户信息中的机构信息查询即可。

为了方便从 SecurityContextHolder 上下文中拿到用户信息,定义工具类

SecurityUtil
/**
* 获取当前用户身份工具类
*/
@Slf4j
public class SecurityUtil {

public static XcUser getUser() {
try {
Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principalObj instanceof String) {
// 取出用户身份信息
String principal = principalObj.toString();
return JSON.parseObject(principal, XcUser.class);
}
throw new RuntimeException();
} catch (Exception e) {
log.error("获取当前登陆用户身份出错", e);
throw new BizException("获取当前登陆用户身份出错");
}
}

@Data
public static class XcUser implements Serializable {
private static final long serialVersionUID = 1L;

private String id;
private String username;
private String salt;
private String wxUnionid;
private String nickname;
private String name;
private String userpic;
private String companyId;
private String utype;
private LocalDateTime birthday;
private String sex;
private String email;
private String cellphone;
private String qq;
private String status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
}

改造查询接口

@ApiOperation("课程查询接口")
@PreAuthorize("hasAuthority('xc_teachmanager_course_list')") // 指定权限标识符
@PostMapping("/list")
public PageResult<CourseBase> list(PageParam pageParam, @RequestBody(required = false) QueryCourseParamsDTO dto) {
SecurityUtil.XcUser user = SecurityUtil.getUser();
Long companyId = null;
if (StringUtils.hasText(user.getCompanyId())) {
companyId = Long.parseLong(user.getCompanyId());
}
return courseBaseService.queryCourseBaseList(companyId, pageParam, dto);
}

相应的,在查询时增加机构条件

/**
* 课程分页查询
*
* @param companyId 培训结构ID
* @param pageParam 分页参数
* @param dto 查询参数
*/
@Override
public PageResult<CourseBase> queryCourseBaseList(Long companyId, PageParam pageParam, QueryCourseParamsDTO dto) {
LambdaQueryWrapper<CourseBase> wrapper = Wrappers.<CourseBase>lambdaQuery()
.like(StringUtils.hasText(dto.getCourseName()), CourseBase::getName, dto.getCourseName())
.eq(StringUtils.hasText(dto.getAuditStatus()), CourseBase::getAuditStatus, dto.getAuditStatus())
.eq(StringUtils.hasText(dto.getPublishStatus()), CourseBase::getStatus, dto.getPublishStatus())
.eq(companyId != null, CourseBase::getCompanyId, companyId);

Page<CourseBase> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
Page<CourseBase> pageResult = page(page, wrapper);
return new PageResult<>(pageResult.getRecords(), pageResult.getTotal(), pageParam);
}