授权服务器

Spring Security + OAuth2.0_客户端
授权服务器中有4个端点。说明如下:

  • Authorize Endpoint :授权端点,进行授权。
  • Token Endpoint :令牌端点,经过授权拿到对应的Token。
  • lntrospection Endpoint :校验端点,校验Token的合法性。
  • Revocation Endpoint :撤销端点,撤销授权。
Spring Security Oauth2架构

Spring Security + OAuth2.0_ide_02
说明如下:

  • 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获并重定向到认证服务器。
  • 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端。
  • 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端。
  • 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。
Spring Security Oauth2授权码模式

环境搭建
Spring Security + OAuth2.0_spring_03
(2)pom依赖

        <!-- spring cloud中的oauth2依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <!-- spring cloud中的security依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <!-- web模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

(3)pojo
在pojo包下自定一个实体类User,但是此类必实现UserDetails接口。如下:

package com.sec.kun.pojo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

/**
 * @Author zhoukun
 * @Date 2021/2/17 15:45
 */
/*
 * 自定义User类,需实现UserDetails接口
 */
public class User implements UserDetails {

    private String username;

    private String password;

    private List<GrantedAuthority> authorities;

    // 构造方法
    public User(String username, String password, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

(4)Spring Security配置类

config包下创建SecurityConfig配置类,如下:

package com.sec.kun.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @Author zhoukun
 * @Date 2021/2/17 15:55
 */
/*
 * Spring Security配置类
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**","/login/**","/logout/**").permitAll()//放行
                .anyRequest().authenticated()//其他路径拦截
                .and()
                .formLogin().permitAll()//表单提交放行
                .and()
                .csrf().disable();//csrf关闭
    }

    // 注册PasswordEncoder
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

(5)自定义登录逻辑

service包下创建UserDetailsServiceImpl类,如下:

package com.sec.kun.service.Impl;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import com.sec.kun.pojo.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * @Author zhoukun
 * @Date 2021/2/17 15:56
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //实际是根据用户名去数据库查,这里就直接用静态数据了
        if(!username.equals("86547462")) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 密码加密
        String password = passwordEncoder.encode("123456");
        //创建User用户,自定义的User
        User user = new User(username,password, AuthorityUtils.
                commaSeparatedStringToAuthorityList("admin"));
        return user;
    }
}

(6)认证服务配置

在config包下创建认证服务的配置类AuthorizationServerConfig,如下:

package com.sec.kun.config;

/**
 * @Author zhoukun
 * @Date 2021/2/17 15:59
 */

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

/**
 * 授权服务器
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()//内存中
                .withClient("client")//客户端ID
                .secret(passwordEncoder.encode("zk2000208"))//秘钥
                .redirectUris("https://www.bilibili.com")//重定向到的地址
                .scopes("all")//授权范围
                .authorizedGrantTypes("authorization_code");//授权类型为授权码模式
    }
}

(7)资源服务配置

在config包下创建资源服务的配置类

/*
 * 资源服务配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .requestMatchers()
             .antMatchers("/user/**");
    }
    
   
}

(8)controller

在controller包下创建UserController类,如下:

@RestController
@RequestMapping("/user")
public class UserController {
    
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication.getPrincipal();
    }

}
测试

(1)获取授权码

启动项目,访问:http://localhost:8000/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
说明:
http://localhost:8000:这是项目端口。
/oauth/authorize?response_type=code:获取授权码的固定写法。
client_id:这是客户端ID,就是在授权服务中定义的:
Spring Security + OAuth2.0_redis_04
Spring Security + OAuth2.0_服务器_05
Spring Security + OAuth2.0_服务器_06
重定向到了百度首页,并且拿到了授权码。
(2)获取令牌

因为要发送post请求,所以使用postman。

url:http://localhost:8000/oauth/token
Spring Security + OAuth2.0_spring_07
左边的type选择Basic Auth,右边的用户名为客户端ID,密码是定义好的。
发送请求,返回如下:
Spring Security + OAuth2.0_客户端_08

(3)获取资源服务器资源

需要携带通行令牌来获取。还是post请求:http://localhost:8000/user/getCurrentUser
Spring Security + OAuth2.0_redis_09

Spring Security Oauth2密码模式

环境搭建

直接在授权码模式的基础上进行修改了。

(1)修改SecurityConfig

直接在里面加:

  //注册AuthenticationManager
    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception {
        return super.authenticationManager();
    }

(2)修改AuthorizationServerConfig

直接在里面加:

   /**
     * 密码模式
     */
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    //密码模式需要配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsServiceImpl);
    }

然后下面添加密码模式:

Spring Security + OAuth2.0_服务器_10

测试

(1)获取Token令牌

启动项目,直接在postman中发送:http://localhost:8000/oauth/token
Spring Security + OAuth2.0_服务器_11
Spring Security + OAuth2.0_redis_12
(2)获取资源

现在直接可以携带令牌去访问资源:http://localhost:8000/user/getCurrentUser
Spring Security + OAuth2.0_redis_13

Redis中存储Token令牌

将token直接存在内存中,这在生产环境中是不合理的,下面将其改造成存储在Redis中。

(1)pom依赖

在pom中添加如下依赖:

<!-- redis依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

(2)yml配置

在application.yml中添加:

spring:
  redis:
    host: localhost
    port: 6379
    password: 123456

在AuthorizationServerConfig加入

 /**
     * redis工厂,默认使用lettue
     */
    @Autowired
    public RedisConnectionFactory redisConnectionFactory;

修改
Spring Security + OAuth2.0_redis_14
(5)测试
启动工程,使用密码模式获取令牌:
Spring Security + OAuth2.0_redis_15

查看redis:Spring Security + OAuth2.0_客户端_16

SpringSecurity + OAuth2.0 + JWT

前面只使用Oauth2.0的话,颁发的通行令牌长度太短了,现在想整合JWT,将颁发的token转换一下,转换成jwt格式的长令牌。

JWT:JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

整合JWT
直接在此工程的基础上修改了。
Spring Security + OAuth2.0_spring_17

pom
Spring Security + OAuth2.0_服务器_18
(2)Redis配置类

注释掉Redis的配置类。
(3)Jwt配置类

在config包下创建JwtTokenStoreConfig配置类,如下:

package com.sec.kun.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @Author zhoukun
 * @Date 2021/2/17 19:50
 */
@Configuration
public class JwtTokenStoreConfig {
    //注册JwtAccessTokenConverter
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //配置jwt秘钥
        jwtAccessTokenConverter.setSigningKey("zhoukun");
        return jwtAccessTokenConverter;
    }

    //注册TokenStore
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
}

(4)修改授权配置类

修改AuthorizationServerConfig类,如下:

 /**
     *Jwt配置类
     */
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    TokenStore tokenStore;
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
      endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsServiceImpl)
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)


        ;
    }

Spring Security + OAuth2.0_客户端_19
(5)测试
使用密码模式获取jwt令牌,如下:
Spring Security + OAuth2.0_spring_20
现在的access_token令牌的长度发生了变化,与它对应的是jti值。解析这个token值:
Spring Security + OAuth2.0_spring_21

扩展JWT的内容

现在想往JWT令牌中添加自定义的内容,过程如下。

(1)Jwt内容增强器

创建一个jwt包,包下创建一个Jwt的内容增强器JwtTokenEnhancer,如下:

package com.sec.kun.hancer;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author zhoukun
 * @Date 2021/2/17 19:56
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication oAuth2Authentication) {
        //自定义的内容存到map中
        Map<String,Object> map = new HashMap<>();
        map.put("city","西安");
        map.put("like","yu");
        map.put("age",20);
        map.put("name","泡泡茶壶");
        //下转型
        if(accessToken instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken)accessToken;
            defaultOAuth2AccessToken.setAdditionalInformation(map);
            return defaultOAuth2AccessToken;
        }
        return null;
    }
}

(2)修改Jwt配置类

修改Jwt配置类JwtTokenStoreConfig,如下:
Spring Security + OAuth2.0_服务器_22

    //密码模式需要配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
            throws Exception {
        //创建TokenEnhancerChain实例
        TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();
        List<TokenEnhancer> enhancerList=new ArrayList<>();
        //配置jtv内容增强器
        enhancerList.add(jwtTokenEnhancer);
        enhancerList.add(jwtAccessTokenConverter);
        tokenEnhancerChain.setTokenEnhancers(enhancerList);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsServiceImpl)
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)

        ;
    }

(4)测试
Spring Security + OAuth2.0_redis_23
解析生成的jwt令牌:
Spring Security + OAuth2.0_客户端_24

解析JWT的内容

JWT令牌的内容一般要在java程序中解析出来,以下演示过程。

(1)pom依赖

还是使用jjwt来解析。pom中添加依赖:

<!-- jjwt依赖 -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>

(2)controller
修改UserController,如下:

    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
        //获取请求头的指定内容
        String header = request.getHeader("Authorization");
        //截取,去掉请求头的前6位,获取token
        String token = header.substring(header.indexOf("bearer") + 7);
        //解析Token,获取Claims对象
        Claims claims = Jwts.parser()
                .setSigningKey("zhoukun".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
        return claims;
    }

测试
获取令牌

带着令牌获取资源
Spring Security + OAuth2.0_客户端_25
获取到了,返回的是jwt令牌解析后的内容。

JWT刷新令牌

在Spring Cloud Security中使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token,只需修改认证服务器的配置,添加refresh_token的授权模式即可。

修改授权服务配置类AuthorizationServerConfig,如下:
Spring Security + OAuth2.0_ide_26
测试:1分钟后再发请求:
Spring Security + OAuth2.0_服务器_27
获取不到资源了,现在jwt通行令牌已经过期了。解决方法是加一个刷新令牌refresh_token。如下:
Spring Security + OAuth2.0_客户端_28
然后再启动工程获取令牌:
Spring Security + OAuth2.0_ide_29
等1分钟,用通行令牌获取资源:
Spring Security + OAuth2.0_spring_30
通行令牌过期了。现在使用刷新令牌直接从授权服务端获取新的通行令牌:
Spring Security + OAuth2.0_服务器_31
Spring Security + OAuth2.0_ide_32
Spring Security + OAuth2.0_redis_33
获取到了新的通行令牌和刷新令牌。同样的,通行令牌的有效期还是1分钟,刷新令牌是1小时,再用这个新的通行令牌获取资源:
Spring Security + OAuth2.0_redis_34
资源获取成功。