赞
踩
有来技术团队2022-01-10 11:23:37博主文章分类:Spring Cloud©著作权
文章标签Spring Cloudspring服务器ide文章分类其它其它阅读数235
本篇是开源全栈项目【有来商城】的总结文档,包括对 Spring Security OAuth2 认证授权的核心思想和源码总结,以及系统就如何实现接口和按钮级别细粒度权限控制进行一一说明,也对日常issue问题进行整理,能够帮助大家少走些弯路。
hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间这里只贴出关键部分代码的打磨,【有来】终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权、鉴权的逻辑,结合小伙伴提出来的建议,。
写这篇文章的除了对一年来项目的阶段性总结,也是希望帮助大家快速理解当下流行的OAuth2认证授权模式,2.0以及其在当下主流的微服务+前后端分离开发模式(Spring Cloud + Vue)的实践应用。
在此之前自己有写过有关 Spring Security OAuth2 + Gateway 统一认证授权+鉴权 和 基于网关统一鉴权的RBAC权限设计的两篇文章:
Spring Cloud实战 | 第六篇:Spring Cloud + Spring Security OAuth2 + JWT实现微服务统一认证鉴权
Spring Cloud实战 | 第十一篇:Spring Cloud Gateway统一鉴权下针对RESTful接口的RBAC权限设计方案,附Vue按钮权限控制
本篇可以说是在项目升级后对上面两篇文章的总结。
youlai-mall 是基于Spring Boot 2.5.0、Spring Cloud 2020 、Spring Cloud Alibaba 2021、vue、element-ui、uni-app快速构建的一套全栈开源商城平台,包括后端微服务、前端管理、微信小程序和APP应用。
项目名称 | 码云(Gitee) | Github |
微服务后台 | ||
系统管理前端 | ||
微信小程序 | ||
APP端【暂不更新】 |
码云(Gitee) | GitHub |
线上预览地址
地址: www.youlai.tech 用户名/密码:admin/123456
系统管理端
微信小程序
此次升级2.0版本主要内容和说明整理如下:
ClientDetailsServiceImpl
#loadClientByClientId
方法feign远程获取客户端信息,后续版本计划添加多级缓存提升性能;oauth_client_details
重命名了sys_oauth_client
;/api
,小程序端/APP端请求前缀标识使用/app-api
;如果不是生活所迫,谁愿意一身才华
这句。
项目2.x版本计划事项
OAuth2概念
以下摘自阮一峰老师的文章 OAuth 2.0 的一个简单解释
OAuth2.0是目前最流行的授权机制,用来授权第三方应用,获取用户数据。
简单说,OAuth就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
OAuth2角色
注意: OAuth2所说的资源是用户信息(ID,昵称、性别、头像等),而非微服务资源(商品服务、订单服务等),需要区分开。
OAuth2流程
概念和角色定义这些比较模糊,接下来用【有来项目】演示下OAuth2整个流程,方便快速理解OAuth2,先看下整个项目架构流程图
流程举例:
用户请求订单服务(OAuth2客户端)想获取自己的订单数据 ,但获取订单数据需要用户的资源(比如用户ID),所以需要先到认证中心(OAuth2认证服务器)去认证,认证通过后会返回JWT,接下来用户携带JWT请求订单服务,其中会经过网关(OAuth2资源服务器),网关验证JWT是否有效,验证有效则将携带着用户资源的JWT传递给订单服务,订单服务拿到用户ID之后即可获取到用户的订单数据。
一般资源服务器和认证服务器是同一台服务器,但在这里将资源服务器从认证服务器分离到了网关,个人觉得主要是因为网关的特性,因为所有的服务访问都必须经过网关,可以统一校验JWT的有效性,通过后将携带用户资源的JWT给对应的服务,同样也是契合微服务的单一职责原则,降低耦合度。
OAuth2认证服务器的职责很好理解,提供认证接口,认证通过后返回生成token,对应【有来项目】的youlai-auth认证中心。
认证接口及调试
很多刚接触Spring Security OAuth2的小伙伴不知道其认证接口在哪里。所以这里稍微提一下认证endpoint是/oauth/token,【有来】中重写此认证endpoint,位于AuthController#postAccessToken方法。
Postman认证接口调试
Knife4j认证接口调试(墙裂推荐)
网关youlai-gateway启动后,其服务端口是9999,然后访问 http://localhost:9999/doc.html
点击左侧目录的第二个节点Authorize填写OAuth2的参数完成认证
认证通过后,再点击该微服务的其他接口,会将认证接口生成的token自动填充到请求头中,非常方便和人性化
核心代码
这里只贴出认证中心youlai-auth
关键部分代码,完整代码请从 码云Gitee或 Github获取。
pom依赖
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
安全拦截配置
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Security接口拦截配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/oauth/**").permitAll()
// @link https://gitee.com/xiaoym/knife4j/issues/I1Q5X6 (接口文档knife4j需要放行的规则)
.antMatchers("/webjars/**", "/doc.html", "/swagger-resources/**", "/v2/api-docs").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
认证授权配置
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
private UserDetailsServiceImpl userDetailsService;
private ClientDetailsServiceImpl clientDetailsService;
/**
* OAuth2客户端【数据库加载】
*/
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(clientDetailsService);
}
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer());
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService)
// refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
// 1 重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
// 2 非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
.reuseRefreshTokens(true);
}
/**
* 使用非对称加密算法对token签名
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 从classpath下的密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
KeyPair keyPair = factory.getKeyPair("jwt", "123456".toCharArray());
return keyPair;
}
/**
* JWT内容增强
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
Map<String, Object> additionalInfo = CollectionUtil.newHashMap();
OAuthUserDetails OAuthUserDetails = (OAuthUserDetails) authentication.getUserAuthentication().getPrincipal();
additionalInfo.put("userId", OAuthUserDetails.getId());
additionalInfo.put("username", OAuthUserDetails.getUsername());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false); // 用户不存在异常抛出
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
/**
* 密码编码器
* 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式
* 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
认证授权配置类主要实现功能:
UserDetailService自定义实现加载用户认证信息
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private UserFeignClient userFeignClient;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String clientId = JwtUtils.getAuthClientId();
OAuthClientEnum client = OAuthClientEnum.getByClientId(clientId);
Result result;
OAuthUserDetails oauthUserDetails = null;
switch (client) {
default:
result = userFeignClient.getUserByUsername(username);
if (ResultCode.SUCCESS.getCode().equals(result.getCode())) {
SysUser sysUser = (SysUser)result.getData();
oauthUserDetails = new OAuthUserDetails(sysUser);
}
break;
}
if (oauthUserDetails == null || oauthUserDetails.getId() == null) {
throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
} else if (!oauthUserDetails.isEnabled()) {
throw new DisabledException("该账户已被禁用!");
} else if (!oauthUserDetails.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定!");
} else if (!oauthUserDetails.isAccountNonExpired()) {
throw new AccountExpiredException("该账号已过期!");
}
return oauthUserDetails;
}
}
ClientDetailsService自定义加载客户端认证信息
@Service
@AllArgsConstructor
public class ClientDetailsServiceImpl implements ClientDetailsService {
private OAuthClientFeignClient oAuthClientFeignClient;
@Override
@SneakyThrows
public ClientDetails loadClientByClientId(String clientId) {
try {
Result<SysOauthClient> result = oAuthClientFeignClient.getOAuthClientById(clientId);
if (Result.success().getCode().equals(result.getCode())) {
SysOauthClient client = result.getData();
BaseClientDetails clientDetails = new BaseClientDetails(
client.getClientId(),
client.getResourceIds(),
client.getScope(),
client.getAuthorizedGrantTypes(),
client.getAuthorities(),
client.getWebServerRedirectUri());
clientDetails.setClientSecret(PasswordEncoderTypeEnum.NOOP.getPrefix() + client.getClientSecret());
return clientDetails;
} else {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
} catch (EmptyResultDataAccessException var4) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
}
}
生成密钥库
生成密钥库脚本命令
keytool -genkey -alias jwt -keyalg RSA -keypass 123456 -keystore jwt.jks -storepass 123456
参数说明
-alias 别名
-keyalg 密钥算法
-keypass 密钥口令
-keystore 生成密钥库的存储路径和名称
-storepass 密钥库口令
OAuth2资源服务器是提供给客户端资源的服务器,有验证token的能力,token有效则放开资源,对应【有来项目】的youlai-gateway网关。
核心代码
这里只贴出网关youlai-gateway
关键部分代码,完整代码请从 码云Gitee或 Github获取。
pom依赖
<!-- OAuth2资源服务器-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
统一鉴权管理器
微服务项目最终对外暴露的只有网关服务一个端口,其他微服务端口不对外暴露,所有的请求都会经过网关路由转发到内网微服务上,所以网关是进行接口访问权限校验最好的实践地。
原因有以下:
不过网关鉴权有个需注意的地方,因为项目API设计遵守RESTful接口设计规范,基于RESTful然后我举个例子说,给你一个/youlai-admin/users/1请求路径,你没法判断是获取ID为1的用户信息还是修改ID为1的用户信息,怎么办?
所以将请求方法和请求路径结合生成restfulPath = GET:/youlai-admin/users/1,这样系统就可以进行区分,在设置权限拦截规则的时候需要考虑到,具体的在下文的RBAC权限设计详细说,这里暂只贴出网关鉴权的逻辑代码。
/**
* 网关自定义鉴权管理器
*
* @author <a href="mailto:xianrui0365@163.com">xianrui</a>
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final RedisTemplate redisTemplate;
private final UrlPermRolesLocalCache urlPermRolesLocalCache;
// 是否开启本地缓存
@Value("${local-cache.enabled}")
private Boolean localCacheEnabled;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// 预检请求放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher(); // Ant匹配器
String method = request.getMethodValue();
String path = request.getURI().getPath();
String restfulPath = method + ":" + path; // Restful接口权限设计
// 移动端请求需认证但无需鉴权判断
String token = request.getHeaders().getFirst(AuthConstants.AUTHORIZATION_KEY);
if (pathMatcher.match(GlobalConstants.APP_API_PATTERN, path)) {
// 如果token以"bearer "为前缀,到这里说明JWT有效即已认证
if (StrUtil.isNotBlank(token)
&& token.startsWith(AuthConstants.AUTHORIZATION_PREFIX)) {
return Mono.just(new AuthorizationDecision(true));
} else {
return Mono.just(new AuthorizationDecision(false));
}
}
// 缓存取 URL权限-角色集合 规则数据
// urlPermRolesRules = [{'key':'GET:/api/v1/users/*','value':['ADMIN','TEST']},...]
Map<String, Object> urlPermRolesRules;
if (localCacheEnabled) {
urlPermRolesRules = (Map<String, Object>) urlPermRolesLocalCache.getCache(GlobalConstants.URL_PERM_ROLES_KEY);
if (null == urlPermRolesRules) {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
urlPermRolesLocalCache.setLocalCache(GlobalConstants.URL_PERM_ROLES_KEY, urlPermRolesRules);
}
} else {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
}
// 根据请求路径判断有访问权限的角色列表
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权,默认“没有设置权限规则”不用鉴权
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(Convert.toList(String.class, roles));
if (requireCheck == false) {
requireCheck = true;
}
}
}
if (requireCheck == false) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断JWT中携带的用户角色是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
String roleCode = authority.substring(AuthConstants.AUTHORITY_PREFIX.length()); // 用户的角色
if (GlobalConstants.ROOT_ROLE_CODE.equals(roleCode)) {
return true; // 如果是超级管理员则放行
}
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
return authorizationDecisionMono;
}
}
资源服务器配置
@ConfigurationProperties(prefix = "security")
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private ResourceServerManager resourceServerManager;
@Setter
private List<String> ignoreUrls;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter())
.publicKey(rsaPublicKey()) // 本地获取公钥
//.jwkSetUri() // 远程获取公钥
;
http.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint());
http.authorizeExchange()
.pathMatchers(Convert.toStrArray(ignoreUrls)).permitAll()
.anyExchange().access(resourceServerManager)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler()) // 处理未授权
.authenticationEntryPoint(authenticationEntryPoint()) //处理未认证
.and().csrf().disable();
return http.build();
}
/**
* 未授权自定义响应
*/
@Bean
ServerAccessDeniedHandler accessDeniedHandler() {
return (exchange, denied) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultCode.ACCESS_UNAUTHORIZED));
return mono;
};
}
/**
* token无效或者已过期自定义响应
*/
@Bean
ServerAuthenticationEntryPoint authenticationEntryPoint() {
return (exchange, e) -> {
Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> ResponseUtils.writeErrorInfo(response, ResultCode.TOKEN_INVALID_OR_EXPIRED));
return mono;
};
}
/**
* @return
*ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
* 需要把jwt的Claim中的authorities加入
* 方案:重新定义权限管理器,默认转换器JwtGrantedAuthoritiesConverter
*/
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.JWT_AUTHORITIES_KEY);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
/**
* 本地加载JWT验签公钥
* @return
*/
@SneakyThrows
@Bean
public RSAPublicKey rsaPublicKey() {
Resource resource = new ClassPathResource("public.key");
InputStream is = resource.getInputStream();
String publicKeyData = IoUtil.read(is).toString();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec((Base64.decode(publicKeyData)));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
return rsaPublicKey;
}
}
资源服务器配置类主要实现功能:
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter())
.publicKey(rsaPublicKey()) // 本地获取公钥
//.jwkSetUri() // 远程获取公钥
;
OAuth2资源服务器(网关)在对JWT验签的时候需要使用公钥,通过上面代码可以看到加载公钥有两种方式,分为本地和远程两种方式,下面就两种方式如何实现进行说明,同时也补充下版本2.0新增的本地加载公钥方式中公钥是怎么根据密钥库生成的。
远程加载公钥
认证中心youlai-auth添加获取公钥接口
@ApiOperation(value = "获取公钥", notes = "login")
@GetMapping("/public-key")
public Map<String, Object> getPublicKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
网关youlai-gateway配置公钥的远程请求地址
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9999/youlai-auth/oauth/public-key'
本地加载公钥
/**
* 本地加载JWT验签公钥
* @return
*/
@SneakyThrows
@Bean
public RSAPublicKey rsaPublicKey() {
Resource resource = new ClassPathResource("public.key");
InputStream is = resource.getInputStream();
String publicKeyData = IoUtil.read(is).toString();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec((Base64.decode(publicKeyData)));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
return rsaPublicKey;
}
本地加载方式第一步是加载类路径下的公钥pulic.key
,那么这个公钥是怎么生成的?
生成公钥
其实有关公钥的生成,Github项目中一个issue有详细的描述 https://github.com/hxrui/youlai-mall/issues/27
在这里补充下其详细生成过程
首先访问 http://slproweb.com/products/Win32OpenSSL.html 下载OpenSSL ,根据系统选择对应版本
添加OpenSSL安装后的bin路径如D:\Program Files\OpenSSL-Win64\bin
至系统环境变量path中
cmd切换到密钥库jwt.jks所在路径中,执行keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
输入密钥库口令就可以看到生成的公钥,将内容复制到pulic.key
文件即可
重新生成密钥库后,项目需mvn clean,同步更新公钥内容,否则token验签过不了报token无效
在上一章节提到网关是所有微服务请求的入口,在这里进行统一鉴权是不二之选;不过针对RESTful接口统一鉴权的情况,配置拦截路径的规则需携带请求方法加以区别。
接下来就【有来项目】中如何实现Spring Cloud Gateway + RESTful接口统一拦截鉴权而进行的权限设计进行说明。
RBAC(Role-Based Access Control)基于角色访问控制,目前使用最为广泛的权限模型。
此模型有三个角色用户、角色和权限,在传统的权限模型用户直接关联加了角色层,解耦了用户和权限,使得权限系统有了更清晰的职责划分和更高的灵活度。
这种RBAC权限设计和市面上大差不差,区别的是sys_permission
权限表的设计:
先看下sys_permission权限表的数据,比较下接口权限标识(url_perm)和按钮权限(btn_perm)标识的区别
添加菜单
进入菜单管理页面,进入表单页面,可以看到这是针对vue-router
路由做的菜单设计,系统实现了动态权限路由加载以及路由两种编程式跳转
添加权限
首先选择菜单,右侧关联加载出权限数据,注意这里的关联只是方便权限模块化管理,无实际关联设计
设置URL权限拦截规则,因为是RESTful的接口设计,所以规则中需携带请求Method,在网关鉴权使用Ant匹配器,下图中的*
匹配任意参数
角色授权
进入角色管理页面,点击选择角色→选择菜单→加载权限,勾选设置
上面设置系统管理员
有用户管理、角色管理、菜单管理3个菜单和查看用户和编辑用户2个接口和按钮权限,刷新页面后如下,可以看到页面只有3个菜单,并且新增和删除按钮未在页面显示
添加部门菜单,但未授权查询部门列表权限,刷新页面看到部门管理菜单出现了
点击部门管理菜单请求部门分页列表接口时,提示访问未授权,即接口拦截规则生效
接口权限
在系统管理完成对接口权限的设置,先看下数据库的权限数据
/**
* Spring容器启动完成时加载权限规则至Redis缓存
*/
@Component
@AllArgsConstructor
public class InitPermissionRoles implements CommandLineRunner {
private ISysPermissionService iSysPermissionService;
@Override
public void run(String... args) {
iSysPermissionService.refreshPermRolesRules();
}
}
具体加载详见源码,加载完成后在Redis呈现出来的数据如下
系统管理员
(ADMIN)访问部门列表
接口举例restfulPath = GET:/youlai-admin/api/v1/depts
部门列表
接口的规则
部门列表
接口权限的角色,可以看到有权限的角色集合并没有ADMIN
系统管理员
并没有部门列表
接口的访问权限,则鉴权不通过被拦截
按钮权限
Vue.directive
注册自定义指令v-has-permission
来判断当前登录用户是否拥有按钮权限。看下图就明白如何应用的: vue-element-admin
已自定义过很多的指令,仅需跟着照葫芦画瓢就行。src/directive/permission
路径添加hasPermission.js
文件,编写按钮权限控制代码逻辑
注册v-has-permission
全局指令,在main.js注册成全局指
按钮元素使用自定义指令
最后提一下,用户是在登录成功的时候获取用户信息时拿到的按钮权限标识集合
收集一些项目的issue和被常见的问题。
<mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror>
本篇内容主要涉及OAuth2认证授权模式的原理以及应用,严格遵守微服务单一职责的设计原则,将资源服务器从认证服务器拆分出来,让认证服务器(认证中心)统一负责认证授权,资源服务器(网关)统一处理鉴权,做到功能上的高度解耦。基于RBAC权限模型设计一套适配微服务+前后端分离开发模式的权限框架,在网关统一鉴权的设计基础上实现了对RESTful规范接口的细粒度鉴权;借助vue.directive
自定义指令实现页面的按钮权限控制。总之,【有来】不仅仅是表面上的全栈商城项目,也是一套集成当下主流开发模式、主流技术栈的完整的微服务脚手架项目,没有过度的自定义封装逻辑,容易上手学习和方便二次扩展。最后希望各位道友多多关注开源项目的进展,一起加油,如果项目中遇到问题或者有什么建议,欢迎联系我们。
因为微信交流群超过200人,只能通过邀请方式进入,如果项目中遇到什么问题或者想进入交流群学习的朋友请添加开发人员后由其拉进群,备注“有来”即可。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。