身份的认证服务我们在网关中配置了,授权应该放在各个微服务,为此我们需要在微服务中添加依赖并配置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> { List<String> selectPermissionCodeByUserId (String userId) ; }
在 XcMenuServiceImpl 中实现查询方法
XcMenuServiceImpl @Service public class XcMenuServiceImpl extends ServiceImpl <XcMenuMapper, XcMenu> implements XcMenuService { @Override public List<String> selectPermissionCodeByUserId (String userId) { return baseMapper.selectPermissionCodeByUserId(userId); } }
修改 UserDetailsServiceImpl,增加查询列表的方法
XcMenuServiceImpl private UserDetails getUserPrincipal (XcUserExt xcUserExt) { String password = xcUserExt.getPassword(); String[] authorities = {}; List<String> permissions = xcMenuService.selectPermissionCodeByUserId(xcUserExt.getId()); if (!CollectionUtils.isEmpty(permissions)) { authorities = permissions.toArray(new String [0 ]); } xcUserExt.setPassword(null ); String userJson = JSON.toJSONString(xcUserExt); return User.withUsername(userJson) .password(password) .authorities(authorities).build(); }
集成Spring Security 首先添加Spring Security的依赖信息
pom.xml <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 ); services.setRefreshTokenValiditySeconds(259200 ); 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); }
相应的,在查询时增加机构条件
@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); }