1. Spring Security 与 OAuth2

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,主要内容包括 认证(Authentication) 和 授权(Authorization)!

  • 认证(Authentication)
    • 用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。
    • 常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
  • 授权(Authorization)
    • 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

Outh2的内容详情可以点此查看!

        将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案Spring Security OAuth2。本章我们将使用Spring Security OAuth2构建一个授权服务器来验证用户身份以提供access_token,并使用这个access_token来从资源服务器请求数据。

        

2. Spring Security OAuth2的配置和使用

        

①:引入依赖

如果是spring boot项目,需要引入

//spring整合security
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

//security整合outh2
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

如果是spring cloud项目,需要引入

//只需引入这一个即可:此依赖整合了security、oauth2
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

// spring cloud 
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

②:配置 spring security

@Configuration
@EnableWebSecurity  //这个可以不用写,磁层默认支持了
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
  
  	//自己的类实现了UserDetailsService,注入即可
    @Autowired
    private UserService userService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 获取用户信息
        auth.userDetailsService(userService);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                //登录请求直接通过
                .permitAll()
                //所有请求过来
                .and().authorizeRequests()
                // oauth/** 开头的请求直接通过,因为要访问outh2授权服务器
                .antMatchers("/oauth/**").permitAll()
                //其他所有请求都要进行认证
                .anyRequest().authenticated()
                //登出请求直接通过
                .and().logout().permitAll()
                //解决跨域
                .and().csrf().disable();
    }
    

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // oauth2 密码模式需要拿到这个bean
        return super.authenticationManagerBean();
    }
}

用户信息类UserService

//用户信息可以使用使用一个类 实现UserDetailsService
@Service
public class UserService implements UserDetailsService {

    @Autowired
    @Lazy
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123456");
        //可以去查数据库,这里直接new
        return new User("fox",password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

③:配置授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManagerBean;

    @Autowired
    private UserService userService;

    @Autowired
    private TokenStore tokenStore;


    /**
     * 密码模式需要outh2授权服务器去校验账号、密码
     * 账号、密码在整合security时,放在security中了
     * 所以需要整合security的授权管理器authenticationManagerBean
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
                .tokenStore(tokenStore)  //指定token存储到redis,还有数据库、内存、jwt等存储方式!
                .reuseRefreshTokens(false)  //refresh_token是否重复使用
                .userDetailsService(userService) //这一步包含账号、密码的检查!
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单认证
        security.allowFormAuthenticationForClients()
        // 配置校验token需要带入clientId 和clientSeret配置
        .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        
        /**
         *授权码模式
         *http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
         *http://localhost:8080/oauth/authorize?response_type=code&client_id=client
         *
         * implicit: 简化模式
         *http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
         *
         * password模式
         *  http://localhost:8080/oauth/token?username=fox&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
         *
         *  客户端模式
         *  http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
         *
         *  刷新令牌
         *  http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
         */
        clients.inMemory()
                //配置client_id
                .withClient("client")
                //配置client-secret
                .secret(passwordEncoder.encode("123123"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(3600)
                //配置刷新token的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                /**
                 * 配置grant_type,表示授权类型
                 * authorization_code: 授权码模式
                 * implicit: 简化模式
                 * password: 密码模式
                 * client_credentials: 客户端模式
                 * refresh_token: 更新令牌
                 */
                .authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token");
    }
}

        

④:配置redis、并生成 RedisTokenStore 的token存储方式

spring:
  redis:
    host: 127.0.0.1
    database: 0
@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    public TokenStore tokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

        

2. Spring Security OAuth2 整合 JWT

        不懂JWT的可以点击查看此文章! Spring Security OAuth2 整合 JWT过程与第1节的步骤类似,都有如下几步

  • ①:引入依赖,与第1节步骤不同,此处需要引入jwt依赖
  • ②:配置 spring security
  • ③:配置授权服务器
  • ④:配置 token 存储方式,与第1节步骤不同,此处需要引入JwtTokenStore 的token存储方式!

只不过第①步和第④步稍有不同罢了,此处只将不同点列出!
        

①:引入依赖

以spring cloud项目为例,需要引入

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

// <!--security 整合 jwt-->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

//只需引入这一个即可:此依赖整合了security、oauth2
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

// spring cloud 
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

        

②:生成 JwtTokenStore 的token存储方式

/**
 * 使用Jwt存储token的配置
 */
@Configuration
public class JwtTokenStoreConfig {

    @Bean
    public TokenStore jwtTokenStore(){
        //传入一个 token转换器
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //生成一个 token转换器
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter accessTokenConverter = new
                JwtAccessTokenConverter();
        //配置JWT使用的秘钥、盐值123123
        accessTokenConverter.setSigningKey("123123");
        return accessTokenConverter;
    }

	// JwtTokenEnhancer :自定义增强器
    //把 token增强器 JwtTokenEnhancer 放入容器
    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}

        这样就Spring Security OAuth2 就与 JWT 整合成功了,解密时就必须拿上文配置的盐值123123来进行解密,否则解密失败!

//解密代码!
Jwts.parser()
    .setSigningKey("123123".getBytes(StandardCharsets.UTF_8))
    .parseClaimsJws(token)
    .getBody();

        

如何对 token 进行增强?

Outh2提供了一个接口TokenEnhancer,可以对token进行增强,只需实现该接口即可

//token内容增强器
//注意需要注入容器
@Component
public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
                                     OAuth2Authentication authentication) {
        Map<String, Object> info = new HashMap<>();
        //为原有的token的载荷增加一些内容
        //在对token进行解密时,就可以拿到这里添加的信息
        info.put("enhance", "enhance info");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

然后把该增强器配置进outh2的授权服务器即可!这样在解析token时就可以解析到增强的内容!

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig3 extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManagerBean;

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private JwtTokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置JWT的内容增强器,TokenEnhancer可以对token进行增强!
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        //添加token增强器
        delegates.add(jwtTokenEnhancer);
        //添加转换器
        delegates.add(jwtAccessTokenConverter);
        //把增强内容放入 增强链中
        enhancerChain.setTokenEnhancers(delegates);
        
        endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
                .tokenStore(tokenStore)  //配置存储令牌策略
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(enhancerChain) //配置token增强链
                .reuseRefreshTokens(false)  //refresh_token是否重复使用
                .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单认证
        security.allowFormAuthenticationForClients()
        // 获取密钥需要身份认证,使用单点登录时必须配置
        .tokenKeyAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        /**
         *授权码模式
         *http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:8081/login&scope=all
         *http://localhost:8080/oauth/authorize?response_type=code&client_id=client
         *
         * password模式
         *  http://localhost:8080/oauth/token?username=fox&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
         *
         *  客户端模式
         *  http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
         *
         *  刷新令牌
         *  http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
         */
        clients.inMemory()
                //配置client_id
                .withClient("client")
                //配置client-secret
                .secret(passwordEncoder.encode("123123"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(3600)
                //配置刷新token的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://localhost:8081/login",
                        "http://localhost:8082/login")
                //自动授权配置
                .autoApprove(true)
                //配置申请的权限范围
                .scopes("all")
                /**
                 * 配置grant_type,表示授权类型
                 * authorization_code: 授权码
                 * password: 密码
                 * client_credentials: 客户端
                 * refresh_token: 更新令牌
                 */
                .authorizedGrantTypes("authorization_code","password","refresh_token");
    }


}