Spring Cloud Gateway +Oauth2 +JWT+Vue 實現前後端分離RBAC權限管理

這是一篇很長的文章,所以需要有點耐心,當然也可以直接查看源碼:源碼
對於有不太明白的地方可以給我留言,如果網關是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;
}

對於擴展登錄方式,可以查看源碼。實現比較簡單。這裏就不進行講解了,大家可以研究下源碼。
源碼地址:源碼
下一篇將會講解前端實現
如果大家覺得有幫助,可以打賞下:
打賞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章