這是一篇很長的文章,所以需要有點耐心,當然也可以直接查看源碼:源碼
對於有不太明白的地方可以給我留言,如果網關是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: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
#開啓自動路由
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: auth #權限
uri: lb://oauth2-server
order: 0
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
#redis配置
redis:
host: 127.0.0.1
password:
port: 6379
database: 0
timeout: 60000
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/login
notRole: |
/user
1.4、用戶獲取業務
1.4.1、控制器實現
/**
* 用戶控制器
* @author 大仙
*/
@RestController
public class UserController {
@Autowired
private 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 {
@Autowired
private PermissionUtil permissionUtil;
@Override
public 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/login
notRole: |
/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 {
@Bean
public 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
*/
@Bean
public 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;
@Autowired
private RedisTemplate<String, TokenEntity> redisTemplate;
@Autowired
private RedisTemplate<String, Permission> permissionRedisTemplate;
@Autowired
private PermissionUtil permissionUtil;
private AntPathMatcher matcher = new AntPathMatcher();
@Override
public 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[]{};
}
}
/**
* 構造對象
*/
@Data
class AuthUser{
private String authority;
private BaseUser baseUser;
}
}
該類爲核心類,請仔細進行查看。
1.3、相關過濾器配置
/**
* 跨域配置
* @author 大仙
*/
public class CorsFilter implements WebFilter {
@Override
public 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{
@Override
public 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 {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate() {
RedisTemplate<String, TokenEntity> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new RedisObjectSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
/**
* 存儲權限
* @return
*/
@Bean
public 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();
@Override
public byte[] serialize(Object o) throws SerializationException {
if(o == null) {
return EMPTY_ARRAY;
}
try {
return serializer.convert(o);
}catch (Exception e){
return EMPTY_ARRAY;
}
}
@Override
public 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 {
@Autowired
private 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-server
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
#redis配置
redis:
host: 127.0.0.1
password:
port: 6379
database: 0
timeout: 60000
edu:
auth:
server:
maxClient: 30000
tokenValid: 14400
force: false
startRefresh: false
keyPath: classpath:kevin_key.jks
alias: wecode
secret: wecodeCloud
#sharding-jdbc讀寫分離的配置 ,如果不想讀寫分離配置,設置2個數據庫同源即可
sharding.jdbc:
data-sources:
ds_master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/edu_user?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
ds_slave_0:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/edu_user?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
master-slave-rule:
name: ds_ms
master-data-source-name: ds_master
slave-data-source-names: ds_slave_0
load-balance-algorithm-type: round_robin
props:
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 {
/**
* 用戶詳情業務實現
*/
@Autowired
private UsernameUserDetailService userDetailsService;
@Autowired
private PhoneUserDetailService phoneUserDetailService;
@Autowired
private QrUserDetailService qrUserDetailService;
@Autowired
private OpenIdUserDetailService openIdUserDetailService;
/**
* 重新實例化bean
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 由於使用的是JWT,我們這裏不需要csrf
http.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()
;
}
/**
* 用戶驗證
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(phoneAuthenticationProvider());
auth.authenticationProvider(daoAuthenticationProvider());
auth.authenticationProvider(openIdAuthenticationProvider());
auth.authenticationProvider(qrAuthenticationProvider());
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 設置userDetailsService
provider.setUserDetailsService(userDetailsService);
// 禁止隱藏用戶未找到異常
provider.setHideUserNotFoundExceptions(false);
// 使用BCrypt進行密碼的hash
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PhoneAuthenticationProvider phoneAuthenticationProvider(){
PhoneAuthenticationProvider provider = new PhoneAuthenticationProvider();
// 設置userDetailsService
provider.setUserDetailsService(phoneUserDetailService);
// 禁止隱藏用戶未找到異常
provider.setHideUserNotFoundExceptions(false);
return provider;
}
@Bean
public QrAuthenticationProvider qrAuthenticationProvider(){
QrAuthenticationProvider provider = new QrAuthenticationProvider();
// 設置userDetailsService
provider.setUserDetailsService(qrUserDetailService);
// 禁止隱藏用戶未找到異常
provider.setHideUserNotFoundExceptions(false);
return provider;
}
@Bean
public OpenIdAuthenticationProvider openIdAuthenticationProvider(){
OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
// 設置userDetailsService
provider.setUserDetailsService(openIdUserDetailService);
// 禁止隱藏用戶未找到異常
provider.setHideUserNotFoundExceptions(false);
return provider;
}
/**
* 賬號密碼登錄
* @return
*/
@Bean
public 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
*/
@Bean
public 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
*/
@Bean
public 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
*/
@Bean
public 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
*/
@Bean
public CodeLoginAuthenticationFilter getCodeLoginAuthenticationFilter() {
CodeLoginAuthenticationFilter filter = new CodeLoginAuthenticationFilter();
try {
filter.setAuthenticationManager(this.authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
filter.setAuthenticationSuccessHandler(getLoginSuccessAuth());
filter.setAuthenticationFailureHandler(getLoginFailure());
return filter;
}
@Bean
public WebLoginAuthSuccessHandler getLoginSuccessAuth(){
WebLoginAuthSuccessHandler myLoginAuthSuccessHandler = new WebLoginAuthSuccessHandler();
return myLoginAuthSuccessHandler;
}
@Bean
public WebLoginFailureHandler getLoginFailure(){
WebLoginFailureHandler myLoginFailureHandler = new WebLoginFailureHandler();
return myLoginFailureHandler;
}
@Bean
public LogoutHandler getLogoutHandler(){
WebLogoutHandler myLogoutHandler = new WebLogoutHandler();
return myLogoutHandler;
}
@Bean
public LogoutSuccessHandler getLogoutSuccessHandler(){
WebLogoutSuccessHandler logoutSuccessHandler = new WebLogoutSuccessHandler();
return logoutSuccessHandler;
}
}
/**
* 認證服務配置
* @author 大仙
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private TokenStore authTokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UsernameUserDetailService userDetailService;
@Autowired
private DataSource dataSource;
@Bean("jdbcClientDetailsService")
public ClientDetailsService clientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用JdbcClientDetailsService客戶端詳情服務
clients.withClientDetails(clientDetailsService());
}
@Bean("authTokenStore")
//指定filter用服務端的
@Primary
public TokenStore authTokenStore() {
return new JwtTokenStore(authJwtAccessTokenConverter());
}
/**
* 配置授權服務器端點,如令牌存儲,令牌自定義,用戶批准和授權類型,不包括端點安全配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailService)
.tokenServices(defaultTokenServices());
}
@Primary
@Bean
public 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
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Bean
public AuthServerProperties authServerProperties(){
return new AuthServerProperties();
}
/**
* key
* @return
*/
@Bean
public 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 {
@Autowired
private 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);
@Autowired
private ClientDetailsService jdbcClientDetailsService;
@Autowired
private DefaultTokenServices defaultTokenServices;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private TokenStore authTokenStore;
@Autowired
private RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate;
@Autowired
private AuthServerProperties authServerProperties;
@Override
public 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()) {
//獲取刷新Token
DefaultExpiringOAuth2RefreshToken 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> {
@Autowired
private ObjectMapper objectMapper;
@Override
public 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());
@Autowired
private RedisTemplate<String, TokenEntity> tokenEntityRedisTemplate;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
logger.info("開始執行退出邏輯===");
// 獲取Token
String 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());
@Autowired
private ObjectMapper objectMapper;
@Override
public 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());
/**
* 用戶業務接口
*/
@Autowired
protected UserService userService;
@Autowired
private RedisTemplate<String, Permission> permissionRedisTemplate;
@Override
public 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());
});
// 返回帶有用戶權限信息的User
org.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 {
@Override
protected 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());
@Override
protected 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;
}
@Override
public void eraseCredentials() {
user.eraseCredentials();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return user.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public BaseUser getBaseUser() {
return baseUser;
}
}
/**
* jwt token構造器
* @author 大仙
*/
public class JwtAccessToken extends JwtAccessTokenConverter{
/**
* 生成token
* @param accessToken
* @param authentication
* @return
*/
@Override
public 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
*/
@Override
public 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;
}
對於擴展登錄方式,可以查看源碼。實現比較簡單。這裏就不進行講解了,大家可以研究下源碼。
源碼地址:源碼
下一篇將會講解前端實現
如果大家覺得有幫助,可以打賞下: