美文网首页学习资料
Spring Cloud+OAuth2+Spring Secur

Spring Cloud+OAuth2+Spring Secur

作者: 陈二狗想吃肉 | 来源:发表于2021-02-19 19:23 被阅读0次

    因为目前做了一个基于Spring Cloud的微服务项目,所以了解到了OAuth2,打算整合一下OAuth2来实现统一授权。关于OAuth是一个关于授权的开放网络标准,目前的版本是2.0,这里我就不多做介绍了。下面贴一下我学习过程中参考的资料。

    开发环境:Windows10,  Intellij Idea2018.2,   jdk1.8,  redis3.2.9, Spring Boot 2.0.2 Release, Spring Cloud Finchley.RC2 Spring 5.0.6

    项目目录

    eshop —— 父级工程,管理jar包版本

    eshop-server —— Eureka服务注册中心

    eshop-gateway —— Zuul网关

    eshop-auth —— 授权服务

    eshop-member —— 会员服务

    eshop-email —— 邮件服务(暂未使用)

    eshop-common —— 通用类

    关于如何构建一个基本的Spring Cloud 微服务这里就不赘述了,不会的可以看一下我的关于Spring Cloud系列的博客。这里给个入口地址:https://blog.csdn.net/wya1993/article/category/7701476

    授权服务

    首先构建eshop-auth服务,引入相关依赖

    <?xml version="1.0"encoding="UTF-8"?>

    <project xmlns="http://maven.apache.org/POM/4.0.0"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>

    <artifactId>eshop-parent</artifactId>

    <groupId>com.curise.eshop</groupId>

    <version>1.0-SNAPSHOT</version>

    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>eshop-auth</artifactId>

    <packaging>war</packaging>

    <description>授权模块</description>

    <dependencies>

    <dependency>

    <groupId>com.curise.eshop</groupId>

    <artifactId>eshop-common</artifactId>

    <version>1.0-SNAPSHOT</version>

    </dependency>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-oauth2</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-security</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

    </dependency>

    <dependency>

    <groupId>org.mybatis.spring.boot</groupId>

    <artifactId>mybatis-spring-boot-starter</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>

    <dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

    </dependency>

    <dependency>

    <groupId>com.alibaba</groupId>

    <artifactId>druid</artifactId>

    </dependency>

    <dependency>

    <groupId>log4j</groupId>

    <artifactId>log4j</artifactId>

    </dependency>

    </dependencies>

    <build>

    <plugins>

    <plugin>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-maven-plugin</artifactId>

    </plugin>

    </plugins>

    </build>

    </project>

    接下来,配置Mybatis、redis、eureka,贴一下配置文件

    server:

    port:1203

    spring:

    application:

    name:eshop-auth

    redis:

    database:0

    host:192.168.0.117

    port:6379

    password:

    jedis:

    pool:

    max-active:8

    max-idle:8

    min-idle:0

    datasource:

    driver-class-name:com.mysql.jdbc.Driver

    url:jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true

    username:root

    password:root

    druid:

    initialSize:5#初始化连接大小

    minIdle:5#最小连接池数量

    maxActive:20#最大连接池数量

    maxWait:60000#获取连接时最大等待时间,单位毫秒

    timeBetweenEvictionRunsMillis:60000#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒

    minEvictableIdleTimeMillis:300000#配置一个连接在池中最小生存的时间,单位是毫秒

    validationQuery:SELECT1from DUAL  #测试连接

    testWhileIdle:true#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性

    testOnBorrow:false#获取连接时执行检测,建议关闭,影响性能

    testOnReturn:false#归还连接时执行检测,建议关闭,影响性能

    poolPreparedStatements:false#是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭

    maxPoolPreparedStatementPerConnectionSize:20#开启poolPreparedStatements后生效

    filters:stat,wall,log4j #配置扩展插件,常用的插件有=>stat:监控统计  log4j:日志  wall:防御sql注入

    connectionProperties:'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000'#通过connectProperties属性来打开mergeSql功能;慢SQL记录

    eureka:

    instance:

    prefer-ip-address:true

    instance-id:${spring.cloud.client.ip-address}:${server.port}

    client:

    service-url:

    defaultZone:http://localhost:1111/eureka/

    mybatis:

    type-aliases-package:com.curise.eshop.common.entity

    configuration:

    map-underscore-to-camel-case:true#开启驼峰命名,l_name->lName

    jdbc-type-for-null:NULL

    lazy-loading-enabled:true

    aggressive-lazy-loading:true

    cache-enabled:true#开启二级缓存

    call-setters-on-nulls:true#map空列不显示问题

    mapper-locations:

    -classpath:mybatis/*.xml

    AuthApplication添加@EnableDiscoveryClient和@MapperScan注解。

    接下来配置认证服务器AuthorizationServerConfig ,并添加@Configuration和@EnableAuthorizationServer注解,其中ClientDetailsServiceConfigurer配置在内存中,当然也可以从数据库读取,以后慢慢完善。

    @Configuration

    @EnableAuthorizationServer

    publicclassAuthorizationServerConfigextendsAuthorizationServerConfigurerAdapter{

    @Autowired

    privateAuthenticationManager authenticationManager;

    @Autowired

    privateDataSource dataSource;

    @Autowired

    privateRedisConnectionFactory redisConnectionFactory;

    @Autowired

    privateMyUserDetailService userDetailService;

    @Bean

    publicTokenStoretokenStore(){

    returnnewRedisTokenStore(redisConnectionFactory);

    }

    @Override

    publicvoidconfigure(AuthorizationServerSecurityConfigurer security)throwsException{

    security

    .allowFormAuthenticationForClients()

    .tokenKeyAccess("permitAll()")

    .checkTokenAccess("isAuthenticated()");

    }

    @Override

    publicvoidconfigure(ClientDetailsServiceConfigurer clients)throwsException{

    // clients.withClientDetails(clientDetails());

    clients.inMemory()

    .withClient("android")

    .scopes("read")

    .secret("android")

    .authorizedGrantTypes("password","authorization_code","refresh_token")

    .and()

    .withClient("webapp")

    .scopes("read")

    .authorizedGrantTypes("implicit")

    .and()

    .withClient("browser")

    .authorizedGrantTypes("refresh_token","password")

    .scopes("read");

    }

    @Bean

    publicClientDetailsServiceclientDetails(){

    returnnewJdbcClientDetailsService(dataSource);

    }

    @Bean

    publicWebResponseExceptionTranslatorwebResponseExceptionTranslator(){

    returnnewMssWebResponseExceptionTranslator();

    }

    @Override

    publicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throwsException{

    endpoints.tokenStore(tokenStore())

    .userDetailsService(userDetailService)

    .authenticationManager(authenticationManager);

    endpoints.tokenServices(defaultTokenServices());

    //认证异常翻译

    // endpoints.exceptionTranslator(webResponseExceptionTranslator());

    }

    /**

    *

    注意,自定义TokenServices的时候,需要设置@Primary,否则报错,

    * @return

    */

    @Primary

    @Bean

    publicDefaultTokenServicesdefaultTokenServices(){

    DefaultTokenServices tokenServices=newDefaultTokenServices();

    tokenServices.setTokenStore(tokenStore());

    tokenServices.setSupportRefreshToken(true);

    //tokenServices.setClientDetailsService(clientDetails());

    // token有效期自定义设置,默认12小时

    tokenServices.setAccessTokenValiditySeconds(60*60*12);

    // refresh_token默认30天

    tokenServices.setRefreshTokenValiditySeconds(60*60*24*7);

    returntokenServices;

    }

    }

    在上述配置中,认证的token是存到redis里的,如果你这里使用了Spring5.0以上的版本的话,使用默认的RedisTokenStore认证时会报如下异常:

    nested exception is java.lang.NoSuchMethodError:org.springframework.data.redis.connection.RedisConnection.set([B[B)V

    原因是spring-data-redis 2.0版本中set(String,String)被弃用了,要使用RedisConnection.stringCommands().set(…),所有我自定义一个RedisTokenStore,代码和RedisTokenStore一样,只是把所有conn.set(…)都换成conn..stringCommands().set(…),测试后方法可行。

    publicclassRedisTokenStoreimplementsTokenStore{

    privatestaticfinalString ACCESS="access:";

    privatestaticfinalString AUTH_TO_ACCESS="auth_to_access:";

    privatestaticfinalString AUTH="auth:";

    privatestaticfinalString REFRESH_AUTH="refresh_auth:";

    privatestaticfinalString ACCESS_TO_REFRESH="access_to_refresh:";

    privatestaticfinalString REFRESH="refresh:";

    privatestaticfinalString REFRESH_TO_ACCESS="refresh_to_access:";

    privatestaticfinalString CLIENT_ID_TO_ACCESS="client_id_to_access:";

    privatestaticfinalString UNAME_TO_ACCESS="uname_to_access:";

    privatefinalRedisConnectionFactory connectionFactory;

    privateAuthenticationKeyGenerator authenticationKeyGenerator=newDefaultAuthenticationKeyGenerator();

    privateRedisTokenStoreSerializationStrategy serializationStrategy=newJdkSerializationStrategy();

    privateString prefix="";

    publicRedisTokenStore(RedisConnectionFactory connectionFactory){

    this.connectionFactory=connectionFactory;

    }

    publicvoidsetAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator){

    this.authenticationKeyGenerator=authenticationKeyGenerator;

    }

    publicvoidsetSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy){

    this.serializationStrategy=serializationStrategy;

    }

    publicvoidsetPrefix(String prefix){

    this.prefix=prefix;

    }

    privateRedisConnectiongetConnection(){

    returnthis.connectionFactory.getConnection();

    }

    privatebyte[]serialize(Object object){

    returnthis.serializationStrategy.serialize(object);

    }

    privatebyte[]serializeKey(String object){

    returnthis.serialize(this.prefix+object);

    }

    privateOAuth2AccessTokendeserializeAccessToken(byte[]bytes){

    return(OAuth2AccessToken)this.serializationStrategy.deserialize(bytes,OAuth2AccessToken.class);

    }

    privateOAuth2AuthenticationdeserializeAuthentication(byte[]bytes){

    return(OAuth2Authentication)this.serializationStrategy.deserialize(bytes,OAuth2Authentication.class);

    }

    privateOAuth2RefreshTokendeserializeRefreshToken(byte[]bytes){

    return(OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes,OAuth2RefreshToken.class);

    }

    privatebyte[]serialize(String string){

    returnthis.serializationStrategy.serialize(string);

    }

    privateStringdeserializeString(byte[]bytes){

    returnthis.serializationStrategy.deserializeString(bytes);

    }

    @Override

    publicOAuth2AccessTokengetAccessToken(OAuth2Authentication authentication){

    String key=this.authenticationKeyGenerator.extractKey(authentication);

    byte[]serializedKey=this.serializeKey(AUTH_TO_ACCESS+key);

    byte[]bytes=null;

    RedisConnection conn=this.getConnection();

    try{

    bytes=conn.get(serializedKey);

    }finally{

    conn.close();

    }

    OAuth2AccessToken accessToken=this.deserializeAccessToken(bytes);

    if(accessToken!=null){

    OAuth2Authentication storedAuthentication=this.readAuthentication(accessToken.getValue());

    if(storedAuthentication==null||!key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))){

    this.storeAccessToken(accessToken,authentication);

    }

    }

    returnaccessToken;

    }

    @Override

    publicOAuth2AuthenticationreadAuthentication(OAuth2AccessToken token){

    returnthis.readAuthentication(token.getValue());

    }

    @Override

    publicOAuth2AuthenticationreadAuthentication(String token){

    byte[]bytes=null;

    RedisConnection conn=this.getConnection();

    try{

    bytes=conn.get(this.serializeKey("auth:"+token));

    }finally{

    conn.close();

    }

    OAuth2Authentication auth=this.deserializeAuthentication(bytes);

    returnauth;

    }

    @Override

    publicOAuth2AuthenticationreadAuthenticationForRefreshToken(OAuth2RefreshToken token){

    returnthis.readAuthenticationForRefreshToken(token.getValue());

    }

    publicOAuth2AuthenticationreadAuthenticationForRefreshToken(String token){

    RedisConnection conn=getConnection();

    try{

    byte[]bytes=conn.get(serializeKey(REFRESH_AUTH+token));

    OAuth2Authentication auth=deserializeAuthentication(bytes);

    returnauth;

    }finally{

    conn.close();

    }

    }

    @Override

    publicvoidstoreAccessToken(OAuth2AccessToken token,OAuth2Authentication authentication){

    byte[]serializedAccessToken=serialize(token);

    byte[]serializedAuth=serialize(authentication);

    byte[]accessKey=serializeKey(ACCESS+token.getValue());

    byte[]authKey=serializeKey(AUTH+token.getValue());

    byte[]authToAccessKey=serializeKey(AUTH_TO_ACCESS+authenticationKeyGenerator.extractKey(authentication));

    byte[]approvalKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(authentication));

    byte[]clientId=serializeKey(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId());

    RedisConnection conn=getConnection();

    try{

    conn.openPipeline();

    conn.stringCommands().set(accessKey,serializedAccessToken);

    conn.stringCommands().set(authKey,serializedAuth);

    conn.stringCommands().set(authToAccessKey,serializedAccessToken);

    if(!authentication.isClientOnly()){

    conn.rPush(approvalKey,serializedAccessToken);

    }

    conn.rPush(clientId,serializedAccessToken);

    if(token.getExpiration()!=null){

    intseconds=token.getExpiresIn();

    conn.expire(accessKey,seconds);

    conn.expire(authKey,seconds);

    conn.expire(authToAccessKey,seconds);

    conn.expire(clientId,seconds);

    conn.expire(approvalKey,seconds);

    }

    OAuth2RefreshToken refreshToken=token.getRefreshToken();

    if(refreshToken!=null&&refreshToken.getValue()!=null){

    byte[]refresh=serialize(token.getRefreshToken().getValue());

    byte[]auth=serialize(token.getValue());

    byte[]refreshToAccessKey=serializeKey(REFRESH_TO_ACCESS+token.getRefreshToken().getValue());

    conn.stringCommands().set(refreshToAccessKey,auth);

    byte[]accessToRefreshKey=serializeKey(ACCESS_TO_REFRESH+token.getValue());

    conn.stringCommands().set(accessToRefreshKey,refresh);

    if(refreshTokeninstanceofExpiringOAuth2RefreshToken){

    ExpiringOAuth2RefreshToken expiringRefreshToken=(ExpiringOAuth2RefreshToken)refreshToken;

    Date expiration=expiringRefreshToken.getExpiration();

    if(expiration!=null){

    intseconds=Long.valueOf((expiration.getTime()-System.currentTimeMillis())/1000L)

    .intValue();

    conn.expire(refreshToAccessKey,seconds);

    conn.expire(accessToRefreshKey,seconds);

    }

    }

    }

    conn.closePipeline();

    }finally{

    conn.close();

    }

    }

    privatestaticStringgetApprovalKey(OAuth2Authentication authentication){

    String userName=authentication.getUserAuthentication()==null?"":authentication.getUserAuthentication().getName();

    returngetApprovalKey(authentication.getOAuth2Request().getClientId(),userName);

    }

    privatestaticStringgetApprovalKey(String clientId,String userName){

    returnclientId+(userName==null?"":":"+userName);

    }

    @Override

    publicvoidremoveAccessToken(OAuth2AccessToken accessToken){

    this.removeAccessToken(accessToken.getValue());

    }

    @Override

    publicOAuth2AccessTokenreadAccessToken(String tokenValue){

    byte[]key=serializeKey(ACCESS+tokenValue);

    byte[]bytes=null;

    RedisConnection conn=getConnection();

    try{

    bytes=conn.get(key);

    }finally{

    conn.close();

    }

    OAuth2AccessToken accessToken=deserializeAccessToken(bytes);

    returnaccessToken;

    }

    publicvoidremoveAccessToken(String tokenValue){

    byte[]accessKey=serializeKey(ACCESS+tokenValue);

    byte[]authKey=serializeKey(AUTH+tokenValue);

    byte[]accessToRefreshKey=serializeKey(ACCESS_TO_REFRESH+tokenValue);

    RedisConnection conn=getConnection();

    try{

    conn.openPipeline();

    conn.get(accessKey);

    conn.get(authKey);

    conn.del(accessKey);

    conn.del(accessToRefreshKey);

    // Don't remove the refresh token - it's up to the caller to do that

    conn.del(authKey);

    List<Object>results=conn.closePipeline();

    byte[]access=(byte[])results.get(0);

    byte[]auth=(byte[])results.get(1);

    OAuth2Authentication authentication=deserializeAuthentication(auth);

    if(authentication!=null){

    String key=authenticationKeyGenerator.extractKey(authentication);

    byte[]authToAccessKey=serializeKey(AUTH_TO_ACCESS+key);

    byte[]unameKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(authentication));

    byte[]clientId=serializeKey(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId());

    conn.openPipeline();

    conn.del(authToAccessKey);

    conn.lRem(unameKey,1,access);

    conn.lRem(clientId,1,access);

    conn.del(serialize(ACCESS+key));

    conn.closePipeline();

    }

    }finally{

    conn.close();

    }

    }

    @Override

    publicvoidstoreRefreshToken(OAuth2RefreshToken refreshToken,OAuth2Authentication authentication){

    byte[]refreshKey=serializeKey(REFRESH+refreshToken.getValue());

    byte[]refreshAuthKey=serializeKey(REFRESH_AUTH+refreshToken.getValue());

    byte[]serializedRefreshToken=serialize(refreshToken);

    RedisConnection conn=getConnection();

    try{

    conn.openPipeline();

    conn.stringCommands().set(refreshKey,serializedRefreshToken);

    conn.stringCommands().set(refreshAuthKey,serialize(authentication));

    if(refreshTokeninstanceofExpiringOAuth2RefreshToken){

    ExpiringOAuth2RefreshToken expiringRefreshToken=(ExpiringOAuth2RefreshToken)refreshToken;

    Date expiration=expiringRefreshToken.getExpiration();

    if(expiration!=null){

    intseconds=Long.valueOf((expiration.getTime()-System.currentTimeMillis())/1000L)

    .intValue();

    conn.expire(refreshKey,seconds);

    conn.expire(refreshAuthKey,seconds);

    }

    }

    conn.closePipeline();

    }finally{

    conn.close();

    }

    }

    @Override

    publicOAuth2RefreshTokenreadRefreshToken(String tokenValue){

    byte[]key=serializeKey(REFRESH+tokenValue);

    byte[]bytes=null;

    RedisConnection conn=getConnection();

    try{

    bytes=conn.get(key);

    }finally{

    conn.close();

    }

    OAuth2RefreshToken refreshToken=deserializeRefreshToken(bytes);

    returnrefreshToken;

    }

    @Override

    publicvoidremoveRefreshToken(OAuth2RefreshToken refreshToken){

    this.removeRefreshToken(refreshToken.getValue());

    }

    publicvoidremoveRefreshToken(String tokenValue){

    byte[]refreshKey=serializeKey(REFRESH+tokenValue);

    byte[]refreshAuthKey=serializeKey(REFRESH_AUTH+tokenValue);

    byte[]refresh2AccessKey=serializeKey(REFRESH_TO_ACCESS+tokenValue);

    byte[]access2RefreshKey=serializeKey(ACCESS_TO_REFRESH+tokenValue);

    RedisConnection conn=getConnection();

    try{

    conn.openPipeline();

    conn.del(refreshKey);

    conn.del(refreshAuthKey);

    conn.del(refresh2AccessKey);

    conn.del(access2RefreshKey);

    conn.closePipeline();

    }finally{

    conn.close();

    }

    }

    @Override

    publicvoidremoveAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken){

    this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());

    }

    privatevoidremoveAccessTokenUsingRefreshToken(String refreshToken){

    byte[]key=serializeKey(REFRESH_TO_ACCESS+refreshToken);

    List<Object>results=null;

    RedisConnection conn=getConnection();

    try{

    conn.openPipeline();

    conn.get(key);

    conn.del(key);

    results=conn.closePipeline();

    }finally{

    conn.close();

    }

    if(results==null){

    return;

    }

    byte[]bytes=(byte[])results.get(0);

    String accessToken=deserializeString(bytes);

    if(accessToken!=null){

    removeAccessToken(accessToken);

    }

    }

    @Override

    publicCollection<OAuth2AccessToken>findTokensByClientIdAndUserName(String clientId,String userName){

    byte[]approvalKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(clientId,userName));

    List<byte[]>byteList=null;

    RedisConnection conn=getConnection();

    try{

    byteList=conn.lRange(approvalKey,0,-1);

    }finally{

    conn.close();

    }

    if(byteList==null||byteList.size()==0){

    returnCollections.<OAuth2AccessToken>emptySet();

    }

    List<OAuth2AccessToken>accessTokens=newArrayList<OAuth2AccessToken>(byteList.size());

    for(byte[]bytes:byteList){

    OAuth2AccessToken accessToken=deserializeAccessToken(bytes);

    accessTokens.add(accessToken);

    }

    returnCollections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);

    }

    @Override

    publicCollection<OAuth2AccessToken>findTokensByClientId(String clientId){

    byte[]key=serializeKey(CLIENT_ID_TO_ACCESS+clientId);

    List<byte[]>byteList=null;

    RedisConnection conn=getConnection();

    try{

    byteList=conn.lRange(key,0,-1);

    }finally{

    conn.close();

    }

    if(byteList==null||byteList.size()==0){

    returnCollections.<OAuth2AccessToken>emptySet();

    }

    List<OAuth2AccessToken>accessTokens=newArrayList<OAuth2AccessToken>(byteList.size());

    for(byte[]bytes:byteList){

    OAuth2AccessToken accessToken=deserializeAccessToken(bytes);

    accessTokens.add(accessToken);

    }

    returnCollections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);

    }

    }

    配置资源服务器

    @Configuration

    @EnableResourceServer

    @Order(3)

    publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{

    @Override

    publicvoidconfigure(HttpSecurity http)throwsException{

    http

    .csrf().disable()

    .exceptionHandling()

    .authenticationEntryPoint((request,response,authException)->response.sendError(HttpServletResponse.SC_UNAUTHORIZED))

    .and()

    .requestMatchers().antMatchers("/api/**")

    .and()

    .authorizeRequests()

    .antMatchers("/api/**").authenticated()

    .and()

    .httpBasic();

    }

    }

    配置Spring Security

    @Configuration

    @EnableWebSecurity

    @Order(2)

    publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{

    @Autowired

    privateMyUserDetailService userDetailService;

    @Bean

    publicPasswordEncoderpasswordEncoder(){

    //return new BCryptPasswordEncoder();

    returnnewNoEncryptPasswordEncoder();

    }

    @Override

    protectedvoidconfigure(HttpSecurity http)throwsException{

    http.requestMatchers().antMatchers("/oauth/**")

    .and()

    .authorizeRequests()

    .antMatchers("/oauth/**").authenticated()

    .and()

    .csrf().disable();

    }

    @Override

    protectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{

    auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());

    }

    /**

    * 不定义没有password grant_type

    *

    * @return

    * @throws Exception

    */

    @Override

    @Bean

    publicAuthenticationManagerauthenticationManagerBean()throwsException{

    returnsuper.authenticationManagerBean();

    }

    }

    可以看到ResourceServerConfig 是比SecurityConfig 的优先级低的。

    二者的关系:

    ResourceServerConfig 用于保护oauth相关的endpoints,同时主要作用于用户的登录(form login,Basic auth)

    SecurityConfig 用于保护oauth要开放的资源,同时主要作用于client端以及token的认证(Bearer auth)

    所以我们让SecurityConfig优先于ResourceServerConfig,且在SecurityConfig 不拦截oauth要开放的资源,在ResourceServerConfig 中配置需要token验证的资源,也就是我们对外提供的接口。所以这里对于所有微服务的接口定义有一个要求,就是全部以/api开头。

    如果这里不这样配置的话,在你拿到access_token去请求各个接口时会报invalid_token的提示。

    另外,由于我们自定义认证逻辑,所以需要重写UserDetailService

    @Service("userDetailService")

    publicclassMyUserDetailServiceimplementsUserDetailsService{

    @Autowired

    privateMemberDao memberDao;

    @Override

    publicUserDetailsloadUserByUsername(String memberName)throwsUsernameNotFoundException{

    Member member=memberDao.findByMemberName(memberName);

    if(member==null){

    thrownewUsernameNotFoundException(memberName);

    }

    Set<GrantedAuthority>grantedAuthorities=newHashSet<>();

    // 可用性 :true:可用 false:不可用

    booleanenabled=true;

    // 过期性 :true:没过期 false:过期

    booleanaccountNonExpired=true;

    // 有效性 :true:凭证有效 false:凭证无效

    booleancredentialsNonExpired=true;

    // 锁定性 :true:未锁定 false:已锁定

    booleanaccountNonLocked=true;

    for(Role role:member.getRoles()){

    //角色必须是ROLE_开头,可以在数据库中设置

    GrantedAuthority grantedAuthority=newSimpleGrantedAuthority(role.getRoleName());

    grantedAuthorities.add(grantedAuthority);

    //获取权限

    for(Permission permission:role.getPermissions()){

    GrantedAuthority authority=newSimpleGrantedAuthority(permission.getUri());

    grantedAuthorities.add(authority);

    }

    }

    User user=newUser(member.getMemberName(),member.getPassword(),

    enabled,accountNonExpired,credentialsNonExpired,accountNonLocked,grantedAuthorities);

    returnuser;

    }

    }

    密码验证为了方便我使用了不加密的方式,重写了PasswordEncoder,实际开发还是建议使用BCryptPasswordEncoder。

    publicclassNoEncryptPasswordEncoderimplementsPasswordEncoder{

    @Override

    publicStringencode(CharSequence charSequence){

    return(String)charSequence;

    }

    @Override

    publicbooleanmatches(CharSequence charSequence,String s){

    returns.equals((String)charSequence);

    }

    }

    另外,OAuth的密码模式需要AuthenticationManager支持

    @Override

    @Bean

    publicAuthenticationManagerauthenticationManagerBean()throwsException{

    returnsuper.authenticationManagerBean();

    }

    定义一个Controller,提供两个接口,/api/member用来获取当前用户信息,/api/exit用来注销当前用户

    @RestController

    @RequestMapping("/api")

    publicclassMemberController{

    @Autowired

    privateMyUserDetailService userDetailService;

    @Autowired

    privateConsumerTokenServices consumerTokenServices;

    @GetMapping("/member")

    publicPrincipaluser(Principal member){

    returnmember;

    }

    @DeleteMapping(value="/exit")

    publicResultrevokeToken(String access_token){

    Result result=newResult();

    if(consumerTokenServices.revokeToken(access_token)){

    result.setCode(ResultCode.SUCCESS.getCode());

    result.setMessage("注销成功");

    }else{

    result.setCode(ResultCode.FAILED.getCode());

    result.setMessage("注销失败");

    }

    returnresult;

    }

    }

    会员服务配置

    引入依赖

    <?xml version="1.0"encoding="UTF-8"?>

    <project xmlns="http://maven.apache.org/POM/4.0.0"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>

    <artifactId>eshop-parent</artifactId>

    <groupId>com.curise.eshop</groupId>

    <version>1.0-SNAPSHOT</version>

    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>eshop-member</artifactId>

    <packaging>war</packaging>

    <description>会员模块</description>

    <dependencies>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-test</artifactId>

    <scope>test</scope>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-oauth2</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-security</artifactId>

    </dependency>

    <dependency>

    <groupId>com.alibaba</groupId>

    <artifactId>fastjson</artifactId>

    </dependency>

    </dependencies>

    <build>

    <plugins>

    <plugin>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-maven-plugin</artifactId>

    </plugin>

    </plugins>

    </build>

    </project>

    配置资源服务器

    @Configuration

    @EnableResourceServer

    publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{

    @Override

    publicvoidconfigure(HttpSecurity http)throwsException{

    http

    .csrf().disable()

    .exceptionHandling()

    .authenticationEntryPoint((request,response,authException)->response.sendError(HttpServletResponse.SC_UNAUTHORIZED))

    .and()

    .requestMatchers().antMatchers("/api/**")

    .and()

    .authorizeRequests()

    .antMatchers("/api/**").authenticated()

    .and()

    .httpBasic();

    }

    }

    配置文件配置

    spring:

    application:

    name:eshop-member

    server:

    port:1201

    eureka:

    instance:

    prefer-ip-address:true

    instance-id:${spring.cloud.client.ip-address}:${server.port}

    client:

    service-url:

    defaultZone:http://localhost:1111/eureka/

    security:

    oauth2:

    resource:

    id:eshop-member

    user-info-uri:http://localhost:1202/auth/api/member

    prefer-token-info:false

    MemberApplication主类配置

    @SpringBootApplication

    @EnableDiscoveryClient

    @EnableGlobalMethodSecurity(prePostEnabled=true)

    publicclassMemberApplication{

    publicstaticvoidmain(String[]args){

    SpringApplication.run(MemberApplication.class,args);

    }

    }

    提供对外接口

    @RestController

    @RequestMapping("/api")

    publicclassMemberController{

    @GetMapping("hello")

    @PreAuthorize("hasAnyAuthority('hello')")

    publicStringhello(){

    return"hello";

    }

    @GetMapping("current")

    publicPrincipaluser(Principal principal){

    returnprincipal;

    }

    @GetMapping("query")

    @PreAuthorize("hasAnyAuthority('query')")

    publicStringquery(){

    return"具有query权限";

    }

    }

    配置网关

    引入依赖

    <?xml version="1.0"encoding="UTF-8"?>

    <project xmlns="http://maven.apache.org/POM/4.0.0"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>

    <artifactId>eshop-parent</artifactId>

    <groupId>com.curise.eshop</groupId>

    <version>1.0-SNAPSHOT</version>

    </parent>

    <modelVersion>4.0.0</modelVersion>

    <packaging>jar</packaging>

    <artifactId>eshop-gateway</artifactId>

    <description>网关</description>

    <dependencies>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-oauth2</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-security</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-actuator</artifactId>

    </dependency>

    </dependencies>

    <build>

    <plugins>

    <plugin>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-maven-plugin</artifactId>

    </plugin>

    </plugins>

    </build>

    </project>

    配置文件

    server:

    port:1202

    spring:

    application:

    name:eshop-gateway

    #--------------------eureka---------------------

    eureka:

    instance:

    prefer-ip-address:true

    instance-id:${spring.cloud.client.ip-address}:${server.port}

    client:

    service-url:

    defaultZone:http://localhost:1111/eureka/

    #--------------------Zuul-----------------------

    zuul:

    routes:

    member:

    path:/member/**

    serviceId: eshop-member

    sensitiveHeaders: "*"

    auth:

    path: /auth/**

    serviceId: eshop-auth

    sensitiveHeaders: "*"

    retryable: false

    ignored-services: "*"

    ribbon:

    eager-load:

    enabled: true

    host:

    connect-timeout-millis: 3000

    socket-timeout-millis: 3000

    add-proxy-headers: true

    #---------------------OAuth2---------------------

    security:

    oauth2:

    client:

    access-token-uri: http://localhost:${server.port}/auth/oauth/token

    user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize

    client-id: web

    resource:

    user-info-uri:  http://localhost:${server.port}/auth/api/member

    prefer-token-info: false

    #----------------------超时配置-------------------

    ribbon:

    ReadTimeout: 3000

    ConnectTimeout: 3000

    MaxAutoRetries: 1

    MaxAutoRetriesNextServer: 2

    eureka:

    enabled: true

    hystrix:

    command:

    default:

    execution:

    timeout:

    enabled: true

    isolation:

    thread:

    timeoutInMilliseconds: 3500

    ZuulApplication主类

    @SpringBootApplication

    @EnableDiscoveryClient

    @EnableZuulProxy

    @EnableOAuth2Sso

    publicclassZuulApplication{

    publicstaticvoidmain(String[]args){

    SpringApplication.run(ZuulApplication.class,args);

    }

    }

    Spring Security配置

    @Configuration

    @EnableWebSecurity

    @Order(99)

    publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{

    @Override

    protectedvoidconfigure(HttpSecurity http)throwsException{

    http.csrf().disable();

    }

    }

    接下来分别启动eshop-server、eshop-member、eshop-auth、eshop-gateway。

    先发送一个请求测试一下未认证的效果

    获取认证

    使用access_token请求auth服务下的用户信息接口

    使用access_token请求member服务下的用户信息接口

    请求member服务的query接口

    请求member服务的hello接口,数据库里并没有给用户hello权限

    刷新token

    注销

    后续还会慢慢完善,敬请期待!!

    关于代码和数据表sql已经上传到GitHub。地址:

    https://github.com/WYA1993/springcloud_oauth2.0。

    注意把数据库和redis替换成自己的地址

    统一回复一下,有很多人反映获取认证时返回401,如下:

    {

    "timestamp":"2019-08-13T03:25:27.161+0000",

    "status":401,

    "error":"Unauthorized",

    "message":"Unauthorized",

    "path":"/oauth/token"

    }

    原因是在发起请求的时候没有添加Basic Auth认证,如下图:

    ,添加Basic Auth认证后会在headers添加一个认证消息头

    添加Basic Auth认证的信息在代码中有体现:

    客户端信息和token信息从MySQL数据库中获取

    现在客户端信息都是存在内存中的,生产环境肯定不可以这么做,要支持客户端的动态添加或删除,所以我选择把客户端信息存到MySQL中。

    首先,创建数据表,数据表的结构官方已经给出,地址在

    https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

    222

    其次,需要修改一下sql脚本,把主键的长度改为128,LONGVARBINARY类型改为blob,调整后的sql脚本:

    create table oauth_client_details(

    client_idVARCHAR(128)PRIMARY KEY,

    resource_idsVARCHAR(256),

    client_secretVARCHAR(256),

    scopeVARCHAR(256),

    authorized_grant_typesVARCHAR(256),

    web_server_redirect_uriVARCHAR(256),

    authoritiesVARCHAR(256),

    access_token_validity INTEGER,

    refresh_token_validity INTEGER,

    additional_informationVARCHAR(4096),

    autoapproveVARCHAR(256)

    );

    create table oauth_client_token(

    token_idVARCHAR(256),

    token BLOB,

    authentication_idVARCHAR(128)PRIMARY KEY,

    user_nameVARCHAR(256),

    client_idVARCHAR(256)

    );

    create table oauth_access_token(

    token_idVARCHAR(256),

    token BLOB,

    authentication_idVARCHAR(128)PRIMARY KEY,

    user_nameVARCHAR(256),

    client_idVARCHAR(256),

    authentication BLOB,

    refresh_tokenVARCHAR(256)

    );

    create table oauth_refresh_token(

    token_idVARCHAR(256),

    token BLOB,

    authentication BLOB

    );

    create table oauth_code(

    codeVARCHAR(256),authentication BLOB

    );

    create table oauth_approvals(

    userIdVARCHAR(256),

    clientIdVARCHAR(256),

    scopeVARCHAR(256),

    statusVARCHAR(10),

    expiresAt TIMESTAMP,

    lastModifiedAt TIMESTAMP

    );

    --customized oauth_client_details table

    create table ClientDetails(

    appIdVARCHAR(128)PRIMARY KEY,

    resourceIdsVARCHAR(256),

    appSecretVARCHAR(256),

    scopeVARCHAR(256),

    grantTypesVARCHAR(256),

    redirectUrlVARCHAR(256),

    authoritiesVARCHAR(256),

    access_token_validity INTEGER,

    refresh_token_validity INTEGER,

    additionalInformationVARCHAR(4096),

    autoApproveScopesVARCHAR(256)

    );

    调整后的sql脚步也放到了GitHub中,需要的可以自行下载

    然后在eshop_member数据库创建数据表,将客户端信息添加到oauth_client_details表中

    如果你的密码不是明文,记得client_secret需要加密后存储。

    然后修改代码,配置从数据库读取客户端信息

    接下来启动服务测试即可。

    获取授权

    获取用户信息

    刷新token

    打开数据表发现token这些信息并没有存到表中,因为tokenStore使用的是redis方式,我们可以替换为从数据库读取。修改配置

    重启服务再次测试

    查看数据表,发现token数据已经存到表里了。

    相关文章

      网友评论

        本文标题:Spring Cloud+OAuth2+Spring Secur

        本文链接:https://www.haomeiwen.com/subject/bltzxltx.html