这是一篇很长的文章,所以需要有点耐心,当然也可以直接查看源码:源码
对于有不太明白的地方可以给我留言,如果网关是zuul或者不是基于spring cloud的实现的,那其实更简单了1.1、如果是zuul正常实现资源服务起就行,只是核心的manager实现变了一个接口,这个可以参考下面我给的连接地址。
1.2、如果是单纯的spring boot,就只需要吧auth模块和com模块引入即可。无太大的变化,资源服务器配置在业务模块上即可。
觉得还行的点个Star鼓励下。以下开始正文:
首先,针对RBAC这个概念其实网上有很多明确的解释,这里就不进行细说,我这里简单的列出了系统中权限设计:
其次,了解Spring Cloud Gateway。这是Spring自主研发的网关,依赖的是webflux和传统的web是存在一定的差异。对于webflux的使用可以参考:webflux请求构造
第三,我们需要对Oauth2有一定的了解,他首先是Security的一个插件。对于其的了解可以参考
资源服务期配置
Websocket兼容验证
扩展登录方式
限制登录人数
自定义登陆登出
自定义退出登录逻辑
第四,对于spring boot和spring cloud版本映射的认知,可以参考
spring boot版本对照
第五:注册中心和配置中心这里使用的是nacos,对于nacos的相关集成可以参考
注册中心使用
配置中心使用
基于以上认知,我们首先来构建后端项目。这篇文章会说的比较细致,因为版本比较新,所以很多东西都是我自己摸索出来的。

新建外层POM文件,spring boot的版本是 2.1.12.RELEASE

 <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring-cloud.version>Greenwich.SR5</spring-cloud.version><nacos.version>2.1.1.RELEASE</nacos.version></properties><!-- spring boot配置 --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.12.RELEASE</version></parent><dependencies><!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!--防止版本冲突--><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>26.0-jre</version></dependency></dependencies><!-- spring cloud 配置 --><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

第一步先新建网关服务

1.1、POM文件设定

<description>网关服务</description><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><!-- 注册中心 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>${nacos.version}</version></dependency><!-- 配置中心 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId><version>${nacos.version}</version></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>com-provider-api</artifactId><version>${project.version}</version></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>auth-spring-boot-starter</artifactId><version>${project.version}</version></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>edu-pojo</artifactId><version>${project.version}</version><exclusions><exclusion><groupId>com.clark.daxian</groupId><artifactId>mybatis-plugins</artifactId></exclusion></exclusions></dependency></dependencies>

那么这里解释下几个包:
第一个

 <groupId>com.clark.daxian</groupId><artifactId>com-provider-api</artifactId>

这是我自定义的com模块的java包,内容请参考源码:
com模块api
第二个

 <groupId>com.clark.daxian</groupId><artifactId>auth-spring-boot-starter</artifactId>

资源服务期核心配置包,也是gateway实现权限验证的核心控制。内容请参考源码:

授权模块
第三个

 <groupId>com.clark.daxian</groupId><artifactId>edu-pojo</artifactId><version>${project.version}</version><exclusions><exclusion><groupId>com.clark.daxian</groupId><artifactId>mybatis-plugins</artifactId></exclusion></exclusions>

这是实体相关的工具包,这里我屏蔽了自定义的mybtais的包,因为这一块基本上不会用到数据库相关,而且我这里重写mybatis的部分逻辑,引用了sharding-jdbc实现读写分离。所以可能会和webflux起冲突,所以屏蔽掉。
关于mybatis的重写和pojo相关,可以参考源码:
Mybatis实现自定义lang和部分注解
实体相关
后面很多地方会用到,所以在这里进行说明。
1.2、启动类:

/*** 启动类* @author 大仙*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GatewayApplication {public static void main(String[] args) throws Exception {SpringApplication.run(GatewayApplication.class, args);}}

1.3、yml文件配置

server:port: 8661
spring:application:name: gatewaycloud:nacos:discovery:server-addr: 127.0.0.1:8848config:server-addr: 127.0.0.1:8848file-extension: yaml#开启自动路由gateway:discovery:locator:enabled: truelower-case-service-id: trueroutes:- id: auth  #权限uri: lb://oauth2-serverorder: 0predicates:- Path=/auth/**filters:- StripPrefix=1#redis配置redis:host: 127.0.0.1password:port: 6379database: 0timeout: 60000security:oauth2:resourceserver:jwt:jwk-set-uri: http://localhost:8663/pub-key/jwt.json
edu:security:ignored: |/favicon.ico,/user/v2/api-docs/**,/user/webjars/**,/user/swagger-resources/**,/user/*.html,/auth/loginnotRole: |/user

1.4、用户获取业务
1.4.1、控制器实现

/*** 用户控制器* @author 大仙*/
@RestController
public class UserController  {@Autowiredprivate UserService userService;/*** 获取用户信息* @return*/@GetMapping("/user")public Mono<UserResponse> getUserInfo(){return userService.getUserInfoByAccess();}
}

1.4.2 业务层实现

/*** 用户相关业务接口* @author 大仙*/
public interface UserService {/*** 获取用户信息* @return*/Mono<UserResponse> getUserInfoByAccess();
}
/*** 用户业务接口实现* @author 大仙*/
@Service
public class UserServiceImpl implements UserService, CurrentContent {@Autowiredprivate PermissionUtil permissionUtil;@Overridepublic Mono<UserResponse> getUserInfoByAccess() {Mono<JSONObject> tokenInfo = getTokenInfo();return tokenInfo.map(token->{UserResponse userResponse  = new UserResponse();BaseUser baseUser = token.getJSONObject(Constant.USER_INFO).toJavaObject(BaseUser.class);userResponse.setBaseUser(baseUser);JSONArray array = token.getJSONArray("authorities");//查询全部的权限List<Permission> result = permissionUtil.getResultPermission(array);if(!CollectionUtils.isEmpty(result)) {userResponse.setAccess(result.stream().map(Permission::getAuthCode).collect(Collectors.toList()));}return userResponse;});}}

1.4.3、相关实体

/*** 用户信息节课*/
@Data
public class UserResponse implements Serializable {private static final long serialVersionUID = 5291438641174821152L;/*** 用户信息*/private BaseUser baseUser;/*** 权限列表*/private List<String> access;
}

OK,到这里网关相关的配资就结束了,那么这里可能很多人不明白,怎么进行权限控制的。我们注意下YML文件里面的配置

  security:oauth2:resourceserver:jwt:jwk-set-uri: http://localhost:8663/pub-key/jwt.json
edu:security:ignored: |/favicon.ico,/user/v2/api-docs/**,/user/webjars/**,/user/swagger-resources/**,/user/*.html,/auth/loginnotRole: |/user

security.oauth2.resourceserver.jwt.jwk-set-uri:是对token的检查也就是获取公钥的方法,这个地址是认证服务器的地址,接口是我们自己实现的。具体的实现方式,后面我们在讲。
edu.security.ignored:就是白名单列表了
edu.security.notRole:这个其实就是比较有意思,是需要验证,但是不需要具体角色的。也就是所有人登录就能访问的接口,比如获取用户信息。

auth模块配置

首先我们先来看下项目结构

api:模块是提供,认证服务器和资源服务的通用内容的。
center:是认证服务器配置,注意这里的认证服务器是基于spring cloud oauth2配置的,采用的是web的方式,并不是webflux的配置我,webflux的方式我还没弄明白怎么返回token,如果是单纯的只是鉴权,不采用oauth2是可以的,源码里面也有webflux相关内容。
authconfigure:这就是资源服务器相关配置。是下面会详细讲解的内容。
starter:spring的starter的配置包,没有太大的意义。

通用API模块配置

1.1、POM文件配置

    <description>认证相关API</description><dependencies><!--security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>

1.2、实体相关

/*** Token中缓存用户信息* @author 大仙*/
@Data
public class BaseUser implements Serializable {/*** 主键Id*/protected Long id;/*** 数据创建时间*/@JSONField(format = "yyyy-MM-dd HH:mm:ss")protected LocalDateTime createDate = LocalDateTime.now();/*** 用户名称*/private String userName;/*** 邮箱,用户企业人员进行登录*/private String email;/*** 电话号码,用户客户登录*/private String telephone;/*** 头像*/private String headerUrl;}
/*** token存储实体* @author 大仙*/
@Data
public class TokenEntity implements Serializable {/*** 唯一标识*/private String id;/*** token*/private String token;/*** 失效事件*/private LocalDateTime invalidDate;/*** 失效 1 有效  0 无效*/private Integer status = 1;
}

1.3、相关工具类配置

/*** json 工具类* @author 大仙*/
public class JsonUtils {private static ObjectMapper mapper = new ObjectMapper();public JsonUtils() {}public static <T> T serializable(String json, Class<T> clazz) {if (StringUtils.isEmpty(json)) {return null;} else {try {return mapper.readValue(json, clazz);} catch (IOException var3) {return null;}}}public static <T> T serializable(String json, TypeReference<T> reference) {if (StringUtils.isEmpty(json)) {return null;} else {try {return mapper.readValue(json, reference);} catch (IOException var3) {return null;}}}public static String deserializer(Object json) {if (json == null) {return null;} else {try {return mapper.writeValueAsString(json);} catch (JsonProcessingException var2) {return null;}}}static {mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);}
}
/*** token控制工具类* @author 大仙*/
public class TokenUtil implements Serializable {private static final long serialVersionUID = 8617969696670516L;/*** 存储token* @param id* @param redisTemplate* @param token* @return*/public static Boolean pushToken(String id, RedisTemplate<String, TokenEntity> redisTemplate, String token, Date invalid,Integer max){LocalDateTime invalidDate = invalid.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();long size = redisTemplate.opsForList().size(id);TokenEntity tokenEntity = new TokenEntity();tokenEntity.setInvalidDate(invalidDate);tokenEntity.setToken(token);if(size<=0){redisTemplate.opsForList().rightPush(id,tokenEntity);}else{List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(id, 0, size);tokenEntities = tokenEntities.stream().filter(te -> te.getInvalidDate().isAfter(LocalDateTime.now())).collect(Collectors.toList());if(tokenEntities.size()>= max){return false;}tokenEntities.add(tokenEntity);redisTemplate.delete(id);tokenEntities.forEach(te->{redisTemplate.opsForList().rightPush(id,te);});}return true;}/*** 判断token是否有效* @param id* @param redisTemplate* @param token* @return true 有效 false: 无效*/public static Boolean judgeTokenValid(String id, RedisTemplate<String, TokenEntity> redisTemplate, String token){long size = redisTemplate.opsForList().size(id);if(size<=0){return false;}else{List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(id, 0, size);tokenEntities = tokenEntities.stream().filter(te->te.getToken().equals(token)).collect(Collectors.toList());if(CollectionUtils.isEmpty(tokenEntities)){return false;}TokenEntity tokenEntity = tokenEntities.get(0);if(tokenEntity.getInvalidDate().isAfter(LocalDateTime.now())&&tokenEntity.getStatus()==1){return true;}}return false;}/*** 登出* @param id* @param redisTemplate* @param token*/public static void logout(String id, RedisTemplate<String, TokenEntity> redisTemplate, String token){long size = redisTemplate.opsForList().size(id);if(size<=0){redisTemplate.delete(id);}else{List<TokenEntity> tokenEntities = redisTemplate.opsForList().range(id, 0, size);tokenEntities = tokenEntities.stream().filter(te->!te.getToken().equals(token)).collect(Collectors.toList());if(CollectionUtils.isEmpty(tokenEntities)){redisTemplate.delete(id);}redisTemplate.delete(id);tokenEntities.forEach(te->{redisTemplate.opsForList().rightPush(id,te);});}}
}

资源服务器配置

1.1、POM文件配置

    <name>auth-spring-boot-autoconfigure</name><dependencies><!-- Spring Boot 自动装配 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><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-client</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>com-provider-api</artifactId><version>${project.version}</version></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>auth-api-provider</artifactId><version>${project.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>edu-pojo</artifactId><version>${project.version}</version><exclusions><exclusion><groupId>com.clark.daxian</groupId><artifactId>mybatis-plugins</artifactId></exclusion></exclusions></dependency></dependencies>

1.2、核心配置

/*** 资源服务器配置* @author 大仙 */
@EnableWebFluxSecurity
public class SecurityConfig {@Beanpublic SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.cors().and().csrf().disable().authorizeExchange().anyExchange().access(reactiveAuthorizationManager());http.addFilterAt(new CorsFilter(), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE);http.addFilterAt(new ReactiveRequestContextFilter(), SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE);http.oauth2ResourceServer().jwt();return http.build();}/*** 注入授权管理器* @return*/@Beanpublic ReactiveAuthorizationManager reactiveAuthorizationManager(){WebfluxReactiveAuthorizationManager webfluxReactiveAuthorizationManager = new WebfluxReactiveAuthorizationManager();return webfluxReactiveAuthorizationManager;}
}
/*** 自定义授权管理器,核心配置* @author 大仙 */
@Slf4j
@ConfigurationProperties(prefix = "edu.security")
public class WebfluxReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {private String[] ignoreds;private String[] notRoles;@Autowiredprivate RedisTemplate<String, TokenEntity> redisTemplate;@Autowiredprivate RedisTemplate<String, Permission> permissionRedisTemplate;@Autowiredprivate PermissionUtil permissionUtil;private AntPathMatcher matcher = new AntPathMatcher();@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {//获取请求ServerHttpRequest request =  authorizationContext.getExchange().getRequest();//判断当前是否有接口权限String url =request.getPath().value();log.debug("请求url:{}",url);String httpMethod = request.getMethod().name();log.debug("请求方法:{}",httpMethod);//如果是OPTIONS的请求直接放过if(HttpMethod.OPTIONS.name().equals(httpMethod)){return Mono.just(new AuthorizationDecision(true));}log.debug("白名单:"+ Arrays.toString(ignoreds));// 不拦截的请求for (String path : ignoreds) {String temp = path.trim();if (matcher.match(temp, url)) {return Mono.just(new AuthorizationDecision(true));}}log.debug("不需要角色权限判断的接口:{}",Arrays.toString(notRoles));for (String path : notRoles) {String temp = path.trim();if (matcher.match(temp, url)) {//对于不需要验证角色的接口,只要token验证成功返回成功即可return authentication.map(a ->  {if(a.isAuthenticated()){return new AuthorizationDecision(true);}else{return new AuthorizationDecision(false);}}).defaultIfEmpty(new AuthorizationDecision(false));}}//需要进行权限验证的return//过滤验证成功的authentication.filter(a ->  a.isAuthenticated())//转换成Flux.flatMapIterable(a -> {Jwt jwtValue = null;if(a.getPrincipal() instanceof Jwt){jwtValue = (Jwt)a.getPrincipal();}JSONObject tokenInfo = JSONObject.parseObject(JSONObject.toJSONString(jwtValue.getClaims()));BaseUser baseUser = tokenInfo.getJSONObject(Constant.USER_INFO).toJavaObject(BaseUser.class);//存储当前数据List<AuthUser> authUsers = new ArrayList<>();JSONArray array = tokenInfo.getJSONArray("authorities");for (int i = 0;i<array.size();i++){AuthUser authUser = new AuthUser();authUser.setBaseUser(baseUser);authUser.setAuthority(array.get(i).toString());authUsers.add(authUser);}return authUsers;})//转成成权限名称.any(c-> {//检测权限是否匹配//获取当前用户BaseUser baseUser = c.getBaseUser();//判断当前携带的Token是否有效String  token = request.getHeaders().getFirst(Constant.AUTHORIZATION).replace("Bearer ","");if(!TokenUtil.judgeTokenValid(String.valueOf(baseUser.getId()),redisTemplate,token)){return false;}//获取当前权限String authority = c.getAuthority();//通过当前权限码查询可以请求的地址log.debug("当前权限是:{}",authority);List<Permission> permissions = permissionUtil.getResultPermission(authority);permissions = permissions.stream().filter(permission -> StringUtils.isNotBlank(permission.getRequestUrl())).collect(Collectors.toList());//请求URl匹配,放行if(permissions.stream().anyMatch(permission -> matcher.match(permission.getRequestUrl(),url))){return true;}return false;}).map(hasAuthority ->  new AuthorizationDecision(hasAuthority)).defaultIfEmpty(new AuthorizationDecision(false));}/*** 获取当前用户的权限集合* @param authority* @return*/private List<Permission> getPermissions(String authority){String redisKey = Constant.PERMISSIONS+authority;long size = permissionRedisTemplate.opsForList().size(redisKey);List<Permission> permissions = permissionRedisTemplate.opsForList().range(redisKey, 0, size);return permissions;}public void setIgnored(String ignored) {ignored = org.springframework.util.StringUtils.trimAllWhitespace(ignored);if (ignored != null && !"".equals(ignored)) {this.ignoreds = ignored.split(",");} else {this.ignoreds = new String[]{};}}public void setNotRole(String notRole) {notRole = org.springframework.util.StringUtils.trimAllWhitespace(notRole);if (notRole != null && !"".equals(notRole)) {this.notRoles = notRole.split(",");} else {this.notRoles = new String[]{};}}/*** 构造对象*/@Dataclass AuthUser{private String authority;private BaseUser baseUser;}
}

该类为核心类,请仔细进行查看。
1.3、相关过滤器配置

/*** 跨域配置* @author 大仙*/
public class CorsFilter implements WebFilter {@Overridepublic Mono<Void> filter(ServerWebExchange ctx, WebFilterChain chain) {ServerHttpRequest request = ctx.getRequest();if (CorsUtils.isCorsRequest(request)) {ServerHttpResponse response = ctx.getResponse();HttpHeaders headers = response.getHeaders();headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "");headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false");headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");if (request.getMethod() == HttpMethod.OPTIONS) {response.setStatusCode(HttpStatus.OK);return Mono.empty();}}return chain.filter(ctx);}
}

1.4、基于webflux获取上下文配置

/*** ReactiveRequestContextFilter** @author L.cm*/
public class ReactiveRequestContextFilter implements WebFilter{@Overridepublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {return chain.filter(exchange).subscriberContext(ctx -> ReactiveRequestContextHolder.put(ctx, exchange));}}
/*** ReactiveRequestContextHolder** @author L.cm*/
public class ReactiveRequestContextHolder {private static final Class<ServerWebExchange> CONTEXT_KEY = ServerWebExchange.class;/*** Gets the {@code Mono<ServerWebExchange>} from Reactor {@link Context}** @return the {@code Mono<ServerWebExchange>}*/public static Mono<ServerWebExchange> getExchange() {/*** mica中是这么写的,但是我这样写一直会报错 content is null;*/
//      return Mono.subscriberContext()
//              .map(ctx -> ctx.get(CONTEXT_KEY));/*** 下面是我仿照Security中的改写的。*/return Mono.subscriberContext().filter(c -> c.hasKey(CONTEXT_KEY)).flatMap(c -> Mono.just(c.get(CONTEXT_KEY)));}/*** Gets the {@code Mono<ServerHttpRequest>} from Reactor {@link Context}** @return the {@code Mono<ServerHttpRequest>}*/public static Mono<ServerHttpRequest> getRequest() {return ReactiveRequestContextHolder.getExchange().map(ServerWebExchange::getRequest);}/*** Put the {@code ServerWebExchange} to Reactor {@link Context}** @param context  Context* @param exchange ServerWebExchange* @return the Reactor {@link Context}*/public static Context put(Context context, ServerWebExchange exchange) {return context.put(CONTEXT_KEY, exchange);}
}

相关使用:

/*** 上下文使用,获取相关信息* @author 大仙 */
public interface CurrentContent {/*** 获取用户token信息* @return*/default Mono<JSONObject> getTokenInfo(){Mono<JSONObject> baseUser = ReactiveSecurityContextHolder.getContext().switchIfEmpty(Mono.error(new IllegalStateException("ReactiveSecurityContext is empty"))).map(SecurityContext::getAuthentication).map(Authentication::getPrincipal).map(jwt->{Jwt jwtValue = null;if(jwt instanceof Jwt){jwtValue = (Jwt)jwt;}JSONObject tokenInfo = JSONObject.parseObject(JSONObject.toJSONString(jwtValue.getClaims()));return tokenInfo;});return baseUser;}/*** 获取用户信息* @return*/default Mono<BaseUser> getUserInfo(){return getTokenInfo().map(token->token.getJSONObject(Constant.USER_INFO).toJavaObject(BaseUser.class));}/*** 获取当前请求* @return*/default Mono<ServerHttpRequest> getRequest(){return ReactiveRequestContextHolder.getRequest();}
}

1.5、redis相关配置

/*** redis配置* @author 大仙**/
public class RedisConfig {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate() {RedisTemplate<String, TokenEntity> redisTemplate = new RedisTemplate<>();redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new RedisObjectSerializer());redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}/*** 存储权限* @return*/@Beanpublic RedisTemplate<String, Permission> permissionRedisTemplate() {RedisTemplate<String, Permission> redisTemplate = new RedisTemplate<>();redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new RedisObjectSerializer());redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}}
/*** redis编码解码类* @Author: 朱维* @Date 17:38 2019/11/27*/
public class RedisObjectSerializer implements RedisSerializer {static final byte[] EMPTY_ARRAY = new byte[0];private Converter<Object, byte[]> serializer = new SerializingConverter();private Converter<byte[], Object> deserializer = new DeserializingConverter();@Overridepublic byte[] serialize(Object o) throws SerializationException {if(o == null) {return EMPTY_ARRAY;}try {return serializer.convert(o);}catch (Exception e){return EMPTY_ARRAY;}}@Overridepublic Object deserialize(byte[] bytes) throws SerializationException {if(isEmpty(bytes))return null;try {return deserializer.convert(bytes);}catch (Exception e){throw new SerializationException("Cannot deserialize", e);}}private boolean isEmpty(byte[] bytes){return (bytes == null || bytes.length == 0) ;}
}

1.6、相关工具类配置

/*** 权限工具类* @author 大仙*/
public class PermissionUtil {@Autowiredprivate RedisTemplate<String, Permission> permissionRedisTemplate;/*** 根据角色获取权限列表* @param array* @return*/public List<Permission> getResultPermission(JSONArray array){//查询全部的权限List<Permission> allPermissions = allPermissions();List<Permission> result = new ArrayList<>();for(int i = 0;i<array.size();i++){String roleCode = array.getString(i);List<Permission> permissions = getPermissions(roleCode);result.addAll(getAllChild(permissions,allPermissions,null));}if(result.size()>0){result = result.stream().distinct().collect(Collectors.toList());}return result;}/*** 根据角色获取所有的权限* @param roleCode* @return*/public List<Permission> getResultPermission(String  roleCode){//查询全部的权限List<Permission> allPermissions = allPermissions();List<Permission> result = new ArrayList<>();List<Permission> permissions = getPermissions(roleCode);result.addAll(getAllChild(permissions,allPermissions,null));if(result.size()>0){result = result.stream().distinct().collect(Collectors.toList());}return result;}/*** 获取当前用户的权限集合* @param authority* @return*/private List<Permission> getPermissions(String authority){String redisKey = Constant.PERMISSIONS+authority;long size = permissionRedisTemplate.opsForList().size(redisKey);List<Permission> permissions = permissionRedisTemplate.opsForList().range(redisKey, 0, size);return permissions;}/*** 获得所有的子权限* @param permissions* @param allPermissions* @param result* @return*/private List<Permission> getAllChild(List<Permission> permissions,List<Permission> allPermissions,List<Permission> result){//结果集if(result==null){result = new ArrayList<>();result.addAll(permissions);}List<Permission> needFindSub = new ArrayList<>();for(Permission permission:permissions) {//如果重复,去除if(result.stream().anyMatch(p->p.getId().equals(permission.getId()))){continue;}//得到儿子List<Permission> subPer =  allPermissions.stream().filter(desPer->permission.getId().equals(desPer.getParentPermission())).collect(Collectors.toList());result.addAll(subPer);needFindSub.addAll(subPer);}if(needFindSub.size()>0) {return getAllChild(needFindSub, allPermissions, result);}return result;}/*** 获取所有的权限* @return*/private List<Permission> allPermissions(){String redisKey = Constant.PERMISSIONS+Constant.ALL;long size = permissionRedisTemplate.opsForList().size(redisKey);List<Permission> permissions = permissionRedisTemplate.opsForList().range(redisKey, 0, size);return permissions;}}

1.7、相关装配类配置,关于spring自动装配,请自行查询资料。在resources目录下面新建META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.clark.daxian.auth.resource.config.SecurityConfig,\
com.clark.daxian.auth.resource.config.RedisConfig,\
com.clark.daxian.auth.resource.util.PermissionUtil

1.8、在starter模块引入资源服务器配置即可。

    <dependencies><!-- Spring Boot 自动装配 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>auth-spring-boot-autoconfigure</artifactId><version>${project.version}</version></dependency></dependencies>

并新建resources目录,新建spring.provides指定

provides: auth-spring-boot-autoconfigure

到此,资源服务相关配置已经完成。具体相关代码可以参考源码。

认证服务器配置

这里的配置还是基于web进行配置的。说代码之前,我们先说一下关于JWT的话题。JWT的解说在网上有很多,我这里只是简单的介绍下JWT的秘钥和公钥的生成:

jwt生产证书
keytool -genkeypair -alias 别名 -keyalg RSA -keypass 密码 -keystore kevin_key.jks -storepass 密码
查看证书信息
keytool -list -v -keystore kevin_key.jks -storepass 密码
查看公钥
keytool -list -rfc -keystore kevin_key.jks -storepass 密码

所以在开发的第一步我们先用命令生成秘钥并导出。然后存放到resources目录下面。然后我们来看下认证服务器的整体项目结构。

既然是登录授权,这里肯定就涉及到用户相关,用户模块相关的代码请自行阅读源码,这里就不细说了。
用户模块
1.1、我们还是先看POM文件,这里就和资源服务器有区别了。

    <name>auth-center-provider</name><dependencies><dependency><groupId>com.clark.daxian</groupId><artifactId>auth-api-provider</artifactId><version>${project.version}</version></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>com-spring-boot-starter</artifactId><version>${project.version}</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.8.1</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><!-- 注册中心 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>${nacos.version}</version></dependency><!-- 配置中心 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId><version>${nacos.version}</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>com.nimbusds</groupId><artifactId>nimbus-jose-jwt</artifactId><version>8.6</version><scope>compile</scope></dependency><dependency><groupId>com.clark.daxian</groupId><artifactId>edu-pojo</artifactId><version>${project.version}</version></dependency></dependencies>

1.2、关于启动类配置,认证服务器是单独的服务,资源服务器是依托网关存在的。所以认证服务器是存在启动类的。

/*** 启动类* @author 大仙*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableShardingJdbc
public class ServerApplication {public static void main(String[] args) {SpringApplication.run(ServerApplication.class, args);}
}

这里有个注解需要说明下,@EnableShardingJdbc,这个是一个我自定义的注解,意思是否开启读写分离的配置,相关实现在com模块进行查看。
1.3、yml文件配置

server:port: 8663
spring:application:name: oauth2-servercloud:nacos:discovery:server-addr: 127.0.0.1:8848config:server-addr: 127.0.0.1:8848file-extension: yaml#redis配置redis:host: 127.0.0.1password:port: 6379database: 0timeout: 60000
edu:auth:server:maxClient: 30000tokenValid: 14400force: falsestartRefresh: falsekeyPath: classpath:kevin_key.jksalias: wecodesecret: wecodeCloud
#sharding-jdbc读写分离的配置  ,如果不想读写分离配置,设置2个数据库同源即可
sharding.jdbc:data-sources:ds_master:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/edu_user?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8username: rootpassword: 123456ds_slave_0:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/edu_user?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8username: rootpassword: 123456master-slave-rule:name: ds_msmaster-data-source-name: ds_masterslave-data-source-names: ds_slave_0load-balance-algorithm-type: round_robinprops:sql.show: true
#mybatis的配置
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

1.4、核心配置相关

/*** 配置spring security* ResourceServerConfig 是比SecurityConfig 的优先级低的* @author 大仙**/
@Configuration
@EnableWebSecurity
@Order(1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 用户详情业务实现*/@Autowiredprivate UsernameUserDetailService userDetailsService;@Autowiredprivate PhoneUserDetailService phoneUserDetailService;@Autowiredprivate QrUserDetailService qrUserDetailService;@Autowiredprivate OpenIdUserDetailService openIdUserDetailService;/*** 重新实例化bean*/@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 由于使用的是JWT,我们这里不需要csrfhttp.cors().and().csrf().disable().authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll().and().logout().addLogoutHandler(getLogoutHandler()).logoutSuccessHandler(getLogoutSuccessHandler()).and().addFilterBefore(getPhoneLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(getQrLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(getUsernameLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(getOpenIdLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(getCodeLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).authorizeRequests().antMatchers("/oauth/**").permitAll().and().authorizeRequests().antMatchers("/logout/**").permitAll().and().authorizeRequests().antMatchers("/pub-key/jwt.json").permitAll().and().authorizeRequests().antMatchers("/js/**","/favicon.ico").permitAll().and().authorizeRequests().antMatchers("/v2/api-docs/**","/webjars/**","/swagger-resources/**","/*.html").permitAll().and()// 其余所有请求全部需要鉴权认证.authorizeRequests().anyRequest().authenticated();}/*** 用户验证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(phoneAuthenticationProvider());auth.authenticationProvider(daoAuthenticationProvider());auth.authenticationProvider(openIdAuthenticationProvider());auth.authenticationProvider(qrAuthenticationProvider());}@Beanpublic DaoAuthenticationProvider daoAuthenticationProvider(){DaoAuthenticationProvider provider = new DaoAuthenticationProvider();// 设置userDetailsServiceprovider.setUserDetailsService(userDetailsService);// 禁止隐藏用户未找到异常provider.setHideUserNotFoundExceptions(false);// 使用BCrypt进行密码的hashprovider.setPasswordEncoder(passwordEncoder());return provider;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic PhoneAuthenticationProvider phoneAuthenticationProvider(){PhoneAuthenticationProvider provider = new PhoneAuthenticationProvider();// 设置userDetailsServiceprovider.setUserDetailsService(phoneUserDetailService);// 禁止隐藏用户未找到异常provider.setHideUserNotFoundExceptions(false);return provider;}@Beanpublic QrAuthenticationProvider qrAuthenticationProvider(){QrAuthenticationProvider provider = new QrAuthenticationProvider();// 设置userDetailsServiceprovider.setUserDetailsService(qrUserDetailService);// 禁止隐藏用户未找到异常provider.setHideUserNotFoundExceptions(false);return provider;}@Beanpublic OpenIdAuthenticationProvider openIdAuthenticationProvider(){OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();// 设置userDetailsServiceprovider.setUserDetailsService(openIdUserDetailService);// 禁止隐藏用户未找到异常provider.setHideUserNotFoundExceptions(false);return provider;}/*** 账号密码登录* @return*/@Beanpublic UsernamePasswordAuthenticationFilter getUsernameLoginAuthenticationFilter(){UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());filter.setAuthenticationFailureHandler(getLoginFailure());return filter;}/*** 手机验证码登陆过滤器* @return*/@Beanpublic PhoneLoginAuthenticationFilter getPhoneLoginAuthenticationFilter() {PhoneLoginAuthenticationFilter filter = new PhoneLoginAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());filter.setAuthenticationFailureHandler(getLoginFailure());return filter;}/*** 二维码登录过滤器* @return*/@Beanpublic QrLoginAuthenticationFilter getQrLoginAuthenticationFilter() {QrLoginAuthenticationFilter filter = new QrLoginAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());filter.setAuthenticationFailureHandler(getLoginFailure());return filter;}/*** 微信OPENID登录* @return*/@Beanpublic OpenIdLoginAuthenticationFilter getOpenIdLoginAuthenticationFilter() {OpenIdLoginAuthenticationFilter filter = new OpenIdLoginAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());filter.setAuthenticationFailureHandler(getLoginFailure());return filter;}/*** code登录* @return*/@Beanpublic CodeLoginAuthenticationFilter getCodeLoginAuthenticationFilter() {CodeLoginAuthenticationFilter filter = new CodeLoginAuthenticationFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());filter.setAuthenticationFailureHandler(getLoginFailure());return filter;}@Beanpublic WebLoginAuthSuccessHandler getLoginSuccessAuth(){WebLoginAuthSuccessHandler myLoginAuthSuccessHandler = new WebLoginAuthSuccessHandler();return myLoginAuthSuccessHandler;}@Beanpublic WebLoginFailureHandler getLoginFailure(){WebLoginFailureHandler myLoginFailureHandler = new WebLoginFailureHandler();return myLoginFailureHandler;}@Beanpublic LogoutHandler getLogoutHandler(){WebLogoutHandler myLogoutHandler = new WebLogoutHandler();return myLogoutHandler;}@Beanpublic LogoutSuccessHandler getLogoutSuccessHandler(){WebLogoutSuccessHandler logoutSuccessHandler = new WebLogoutSuccessHandler();return logoutSuccessHandler;}
}
/*** 认证服务配置* @author 大仙*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate TokenStore authTokenStore;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UsernameUserDetailService userDetailService;@Autowiredprivate DataSource dataSource;@Bean("jdbcClientDetailsService")public ClientDetailsService clientDetailsService(){return new JdbcClientDetailsService(dataSource);}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {// 使用JdbcClientDetailsService客户端详情服务clients.withClientDetails(clientDetailsService());}@Bean("authTokenStore")//指定filter用服务端的@Primarypublic TokenStore authTokenStore() {return new JwtTokenStore(authJwtAccessTokenConverter());}/*** 配置授权服务器端点,如令牌存储,令牌自定义,用户批准和授权类型,不包括端点安全配置* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints)  {endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailService).tokenServices(defaultTokenServices());}@Primary@Beanpublic DefaultTokenServices defaultTokenServices() {Collection<TokenEnhancer> tokenEnhancers = applicationContext.getBeansOfType(TokenEnhancer.class).values();TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(new ArrayList<>(tokenEnhancers));DefaultTokenServices defaultTokenServices = new DefaultTokenServices();defaultTokenServices.setTokenStore(authTokenStore);//是否可以重用刷新令牌defaultTokenServices.setReuseRefreshToken(false);defaultTokenServices.setSupportRefreshToken(true);defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);return defaultTokenServices;}/*** 配置授权服务器端点的安全* @param oauthServer* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer)  {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()").allowFormAuthenticationForClients();}@Beanpublic AuthServerProperties authServerProperties(){return new AuthServerProperties();}/*** key* @return*/@Beanpublic KeyPair keyPair(){KeyPair keyPair = new KeyStoreKeyFactory(authServerProperties().getKeyPath(),authServerProperties().getSecret().toCharArray()).getKeyPair(authServerProperties().getAlias());return keyPair;}/*** jwt构造* @return*/@Bean("jwtAccessTokenConverter")public JwtAccessTokenConverter authJwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessToken();converter.setKeyPair(keyPair());return converter;}}

其他的配置都是基于这2个配置来实现的,所以要充分的理解这2个配置的含义。
1.5、为资源服务提供获取公钥的接口


/*** jwt相关控制器* @author 大仙*/
@RestController
public class JWTController {@Autowiredprivate KeyPair keyPair;@GetMapping("/pub-key/jwt.json")public Map<String, Object> getKey() {RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAKey key = new RSAKey.Builder(publicKey).build();return new JWKSet(key).toJSONObject();}
}

1.6、自定义登录退出处理逻辑以及返回

/*** @Author: 朱维* @Date 16:33 2019/11/27*/
public class WebLoginAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler implements RsponseUtil<Map> {/*** 配置日志*/private final static Logger logger = LoggerFactory.getLogger(WebLoginAuthSuccessHandler.class);@Autowiredprivate ClientDetailsService jdbcClientDetailsService;@Autowiredprivate DefaultTokenServices defaultTokenServices;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate TokenStore authTokenStore;@Autowiredprivate RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate;@Autowiredprivate AuthServerProperties authServerProperties;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {Map<String,String> result = createToken(request,authentication);getResponseWeb(response,objectMapper,result);logger.info("登录成功");}/*** 创建token* @param request* @param authentication*/private Map<String, String> createToken(HttpServletRequest request, Authentication authentication){String clientId = request.getParameter("client_id");String clientSecret = request.getParameter("client_secret");ClientDetails clientDetails = jdbcClientDetailsService.loadClientByClientId(clientId);//密码工具BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();if (null == clientDetails) {throw new UnapprovedClientAuthenticationException("clientId不存在" + clientId);}//比较secret是否相等else if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {throw new UnapprovedClientAuthenticationException("clientSecret不匹配" + clientId);}TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(),"password");OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);defaultTokenServices.setTokenStore(authTokenStore);logger.info("==="+authentication.getPrincipal());defaultTokenServices.setAccessTokenValiditySeconds(authServerProperties.getTokenValid());//开启刷新功能if(authServerProperties.getStartRefresh()) {defaultTokenServices.setRefreshTokenValiditySeconds(authServerProperties.getRefreshTokenValid());}OAuth2AccessToken token = defaultTokenServices.createAccessToken(oAuth2Authentication);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Map<String,String> result = new HashMap<>();result.put("access_token", token.getValue());result.put("token_Expiration", sdf.format(token.getExpiration()));//开启刷新功能if(authServerProperties.getStartRefresh()) {//获取刷新TokenDefaultExpiringOAuth2RefreshToken refreshToken = (DefaultExpiringOAuth2RefreshToken) token.getRefreshToken();result.put("refresh_token", refreshToken.getValue());result.put("refresh_token_Expiration", sdf.format(refreshToken.getExpiration()));}logger.debug("token:"+token.getValue());//判断token的和方法性String id = String.valueOf(((BaseUserDetail)authentication.getPrincipal()).getBaseUser().getId());if(!TokenUtil.pushToken(id,tokenEntityRedisTemplate,token.getValue(),token.getExpiration(),authServerProperties.getMaxClient())){throw new AuthException("登录限制,同时登录人数过多");}return result;}
}
/*** @Author: 朱维* @Date 1:55 2019/11/28*/
public class WebLoginFailureHandler implements AuthenticationFailureHandler, RsponseUtil<String> {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {String msg = null;if (exception instanceof BadCredentialsException) {msg = "账号或密码错误";} else {msg = exception.getMessage();}response.setStatus(500);getResponseWeb(response,objectMapper,msg);}
}
/*** 退出登录逻辑* @author 大仙*/
public class WebLogoutHandler implements LogoutHandler {private Logger logger = LoggerFactory.getLogger(getClass());@Autowiredprivate RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate;@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {logger.info("开始执行退出逻辑===");// 获取TokenString accessToken = request.getHeader(Constant.AUTHORIZATION);accessToken = accessToken.replace("Bearer ", "");String id = null;if (accessToken != null) {DecodedJWT jwt = JWT.decode(accessToken);id = String.valueOf(jwt.getClaims().get(Constant.USER_INFO).asMap().get("id"));}TokenUtil.logout(id,tokenEntityRedisTemplate,accessToken);logger.info("执行退出成功==");}
}
/*** 退出成功处理逻辑* @author 大仙*/
public class WebLogoutSuccessHandler implements LogoutSuccessHandler, RsponseUtil<String> {private Logger logger = LoggerFactory.getLogger(getClass());@Autowiredprivate ObjectMapper objectMapper;@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {getResponseWeb(response,objectMapper,"退出成功");}
}

基于webflux的实现可以自行查看源码
1.7、用户认证流程
1.7.1、抽象基础认证service,方便扩展登录方式

/*** @Author: 朱维* @Date 17:01 2019/11/27*/
public abstract class BaseUserDetailService implements UserDetailsService {private Logger logger = LoggerFactory.getLogger(this.getClass());/*** 用户业务接口*/@Autowiredprotected UserService userService;@Autowiredprivate RedisTemplate<String, Permission> permissionRedisTemplate;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if(attributes==null){throw new AuthException("获取不到当先请求");}HttpServletRequest request = attributes.getRequest();String clientId = request.getParameter("client_id");User userInfo = getUser(username,clientId);List<GrantedAuthority> authorities = new ArrayList<>() ;//查询角色列表List<Role> roles = userService.listByUser(userInfo.getId()).getData();roles.forEach(role->{//只存储角色,所以不需要做区别判断authorities.add(new SimpleGrantedAuthority(role.getRoleCode()));List<Permission> permissions = userService.listByRole(role.getId()).getData();//存储权限到redis集合,保持颗粒度细化,当然也可以根据用户存储storePermission(permissions,role.getRoleCode());});// 返回带有用户权限信息的Userorg.springframework.security.core.userdetails.User user =new org.springframework.security.core.userdetails.User(StringUtils.isBlank(userInfo.getTelephone())?userInfo.getEmail():userInfo.getTelephone(),userInfo.getPassword(),isActive(userInfo.getLoginStatus()),true,true,true, authorities);BaseUser baseUser = new BaseUser();BeanUtils.copyProperties(userInfo,baseUser);return new BaseUserDetail(baseUser, user);}/*** 存储权限* @param permissions*/private void storePermission(List<Permission> permissions,String roleCode){String redisKey = Constant.PERMISSIONS +roleCode;// 清除 Redis 中用户的角色permissionRedisTemplate.delete(redisKey);permissions.forEach(permission -> {permissionRedisTemplate.opsForList().rightPush(redisKey,permission);});}/*** 获取用户* @param userName* @return*/protected abstract User getUser(String userName,String clientId) ;/*** 是否有效的* @param active* @return*/private boolean isActive(Integer active){if(1==active){return true;}return false;}}

1.7.2、按登录方式进行具体实现

/*** @Author: 朱维* @Date 17:35 2019/11/27*/
@Service
public class UsernameUserDetailService extends BaseUserDetailService {@Overrideprotected User getUser(String email, String clientId) {User user = userService.getUserByEmail(email).getData();if(user==null){throw new AuthException("用户不存在");}return user;}
}
/*** @Author: 朱维* @Date 17:30 2019/11/27*/
@Service
public class PhoneUserDetailService extends BaseUserDetailService {private Logger logger = LoggerFactory.getLogger(this.getClass());@Overrideprotected User getUser(String telephone, String clientId) {User user = userService.getUserByTel(telephone).getData();if(user==null){throw new AuthException("用户不存在");}return user;}
}

。。。。其他的自己看源码,如公众号登录,二维码登录等。
1.8、自定义Token构造

/*** 包装org.springframework.security.core.userdetails.User类* @author 大仙**/
public class BaseUserDetail implements UserDetails, CredentialsContainer {/*** */private static final long serialVersionUID = 1L;/*** 用户*/private final BaseUser baseUser;private final User user;public BaseUserDetail(BaseUser baseUser, User user) {this.baseUser = baseUser;this.user = user;}@Overridepublic void eraseCredentials() {user.eraseCredentials();}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return user.getAuthorities();}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return user.isAccountNonExpired();}@Overridepublic boolean isAccountNonLocked() {return user.isAccountNonLocked();}@Overridepublic boolean isCredentialsNonExpired() {return user.isCredentialsNonExpired();}@Overridepublic boolean isEnabled() {return user.isEnabled();}public BaseUser getBaseUser() {return baseUser;}
}
/*** jwt token构造器* @author 大仙*/
public class JwtAccessToken extends JwtAccessTokenConverter{/*** 生成token* @param accessToken* @param authentication* @return*/@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);// 设置额外用户信息if(authentication.getPrincipal() instanceof BaseUserDetail) {BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();// 将用户信息添加到token额外信息中defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, JSONObject.parseObject(JSONObject.toJSONString(baseUser)));}return super.enhance(defaultOAuth2AccessToken, authentication);}/*** 解析token* @param value* @param map* @return*/@Overridepublic OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map){OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());return oauth2AccessToken;}private void convertData(OAuth2AccessToken accessToken,  Map<String, ?> map) {accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));}private BaseUser convertUserData(Object map) {String json = JsonUtils.deserializer(map);BaseUser user = JsonUtils.serializable(json, BaseUser.class);return user;}
}

1.9、相关配置类实现

/*** 认证服务器配置* @author 大仙*/
@Data
@ConfigurationProperties(prefix = "edu.auth.server")
public class AuthServerProperties  implements Serializable {/*** 最大登录次数*/private Integer maxClient;/*** 最大有效时间,单位秒*/private Integer tokenValid;/*** 是否允许强行登录*/private Boolean force;/*** 是否开启刷新token*/private Boolean startRefresh;/*** 刷新token有效时间*/private Integer refreshTokenValid;/*** 路径*/private Resource keyPath;/*** 别名*/private String alias;/*** 密码*/private String secret;
}

对于扩展登录方式,可以查看源码。实现比较简单。这里就不进行讲解了,大家可以研究下源码。
源码地址:源码
下一篇将会讲解前端实现
如果大家觉得有帮助,可以打赏下:

Spring Cloud Gateway +Oauth2 +JWT+Vue 实现前后端分离RBAC权限管理相关推荐

  1. 「springcloud 2021 系列」Spring Cloud Gateway + OAuth2 + JWT 实现统一认证与鉴权

    通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权. 将采用 Nacos 作为注册中心,Gateway 作为网关,使用 nimbus-jose-jwt JWT 库操作 JWT 令牌 理论介绍 ...

  2. 开发SpringBoot+Jwt+Vue的前后端分离后台管理系统VueAdmin - 前端笔记

    1. 前言 而接下来,我们即将开发一个前后端分离的后台管理系统VueAdmin.权限框架采用spring security,然后相对来说权限模块开发就多点代码,也仅此而已了.对了前端的系统界面也是我们 ...

  3. 基于java Springboot+Vue+shiro前后端分离疫情防疫管理系统设计和实现2.0

    目录 研究背景 主要特性功能: 视频效果演示 : 主要功能截图: 系统首页: 疫情数据分布图模拟: 用户管理: 角色控制: 菜单权限: 每日健康打卡: 历史出行数据: 外出报备申请: 外出请假审核: ...

  4. 鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统

    鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统 1. 项目背景 一.随着公司的快速发展,企业人员和经 ...

  5. 记一次Spring boot 和Vue的前后端分离的入门培训

    记一次Spring boot 和Vue的前后端分离的入门培训 由于公司之前是写C#的,现在要转 Java分布式 + vue,所以进行一次前后端的简单培训. 前端工具和环境: Node.js V10.1 ...

  6. (五)Debian Linux中部署Spring Boot + Vue的前后端分离项目详细过程(arm64/aarch64架构下)

    专题系列往期文章目录 (一)移动端安卓手机改造成linux服务器&Linux中安装软件踩坑历险记 (二)Debian Linux系统中安装oracle JDK1.8详细过程(arm64/aar ...

  7. 视频教程-SpringBoot2+Vue+AntV前后端分离开发项目实战-Java

    SpringBoot2+Vue+AntV前后端分离开发项目实战 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独特的培训思 ...

  8. 视频教程-SpringBoot+Security+Vue前后端分离开发权限管理系统-Java

    SpringBoot+Security+Vue前后端分离开发权限管理系统 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独 ...

  9. B站云E办Vue+SpringBoot前后端分离项目——MVC三层架构搭建后台项目

    本项目来源B站云E办,笔记整理了项目搭建的过程和涉及的知识点.对于学习来说,不是复制粘贴代码即可,要知其然知其所以然.希望我的笔记能为大家提供思路,也欢迎各位伙伴的指正. 项目前端学习笔记目录 B站云 ...

最新文章

  1. 解析Makefile文件的构建规则
  2. 【Android NDK 开发】JNI 动态注册 ( 动态注册流程 | JNI_OnLoad 方法 | JNINativeMethod 结构体 | GetEnv | RegisterNatives )
  3. [计算机网络]探索ICMP协议
  4. java工厂模式学习
  5. linux系统安装klocwork,linux下klocwork的使用
  6. 【渝粤教育】国家开放大学2018年春季 0032-22T农业经济学 参考试题
  7. c语言和远光灯标志,常见的灯光语言有哪些 新手必须知道的车灯语言
  8. 个人 易混淆 高频 高级单词
  9. 【计算机毕业设计】疫情社区管理系统的设计与实现
  10. 工程项目成本费用明细表_项目成本费用明细表
  11. 【程序员节特别推送】搭建一个与技术无关的博客网站(Java后台)
  12. python实现输出日历_python实现输入日期打印日历
  13. linux使用anaconda安装python包
  14. Windows 服务快捷启动命令
  15. word怎么画图,如何用word制作流程图
  16. php后台登录页,后台登录页面模板源码
  17. 程序员装机必备利器列表
  18. 某学校同学聚会三句半
  19. 全栈开发-IDE介绍与设置、字符串格式化、数据类型、for循环
  20. DS18B20测量温度

热门文章

  1. 图像物体分类与检测算法综述
  2. docker-部署lnmp
  3. [V8]找出可能影响性能的代码(模式)
  4. win10部署安装Elasticsearch8.1.2
  5. 文章管理平台PC端(文章分类)
  6. java虚拟机学习笔记之垃圾收集(上)
  7. HackTheBox-baby nginxatsu
  8. a15仿生芯片和骁龙8gen1 哪个好
  9. 输入法与表情栏无缝切换
  10. linux教程for语句,Bash 中的 For 循环详解