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;
}

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

发布了183 篇原创文章 · 获赞 37 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/zhuwei_clark/article/details/104538792