零基础学习SpringSecurity OAuth2 四种授权模式(理论+实战)

2022-09-19 11:58:06 浏览数 (1)

背景

前段时间有同学私信我,让我讲下Oauth2授权模式,并且还强调是零基础的那种,我也不太理解这个零基础到底是什么程度,但是我觉得任何阶段的同学看完我这个视频,对OAuth2的理解将会有很大的提升,并且也会熟练的使用SpringSecurity OAuth2,轻松搭建认证服务和资源服务。

大概了解下 SpringSecurity

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一套filterChanin进行拦截和过滤

再来了解OAuth2

Oauth2是一个关于授权的官方标准,核心思路是通过各种认证手段(需要用户自己选择实现)进行认证用户身份,并颁发token,使得第三方应用可以使用该令牌在限定时间和限定范围内访问指定资源

再来了解SpringSecurity Oauth2

Spring Security OAuth2建立在Spring Security的基础之上,实现了OAuth2的规范 概念部分铺垫完成了,现在我们讲下OAuth2的四种授权模式

四种授权模式

为什么提供四种呢?四种授权模式代表OAuth授权第三方的不同互信程度

授权码模式(最安全,也最常用)

通过授权码获取token

场景

当用户使用B网站的账号(如微信)授权登录A网站

步骤

  • 跳转到B网站授权页面,输入B网站用户名和密码
  • B网站认证服务验证通过,回调网站A提供的回调地址,并带上认证code
  • A网站收到回调之后,提取code,再调用B网站获取token的接口,并带上A网站在B网站上面注册的client_id和client_secert

别说话,看图

简化模式(有中间人攻击的风险)

简化模式在授权码模式的基础上减去了获取授权码的步骤,B网站认证通过之后,直接返回token 回调给A网站,所以又称为(授权码)“隐藏式”(implicit)

步骤

  • 跳转到B网站授权页面,输入B网站用户名和密码
  • B网站认证服务验证通过,回调网站A提供的回调地址,并带上token信息

看图

密码模式(除非没得选才用,毕竟涉及到用户隐私)

A网站要求用户直接输入用户名和密码,A网站自己拿着用户的密码和账号去B网站进行认证,直接获取token

步骤

  • A网站要求用户输入账号和密码
  • A网站携带账号和密码去B网站认证
  • B网站认证通过,直接返回token给A网站

看图

客户端模式

客户端模式其实和用户就没关系了,其实也可以说它不属于Oauth了,因为是用网站A自己的身份信息去B网站认证获取token,A网站上的所有用户去访问B网站的资源,都是用的A网站自己的客户端信息。

场景

这个一般用于微服务之间进行认证,如服务A想访问服务B,那么服务B需要校验服务A有没有权限访问自己,所以就需要校验服务B的token,这时就可以采用客户端模式,后面讲资源服务去认证中check_toekn的时候,用的就是这种方式。

看图

现在四种授权模式讲完了,大家是不是有个疑问:前面三种授权方式,拿到token之后,是怎么和用户进行绑定的呢?接下来看下实战过程中,是怎么用SpringSecurity Oauth2 实现四种授权方式。

准备好电脑,开干

创建认证服务

创建ClientDetailsServiceImpl

根据clientID获取客户端信息,security会去调用多次,所以一般加上缓存

代码语言:javascript复制
package com.lglbc.oauth2.config.details.client;

import com.lglbc.oauth2.entity.OauthClient;
import com.lglbc.oauth2.enums.PasswordEncoderTypeEnum;
import com.lglbc.oauth2.service.OauthClientService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
 * @author: 乐哥聊编程(全平台同号)
 */
@Service
@RequiredArgsConstructor
public class ClientDetailsServiceImpl implements ClientDetailsService {
    private final OauthClientService oauthClientService;

    @Override
    public ClientDetails loadClientByClientId(String clientId) {
        // 获取client信息
        OauthClient oauthClient = oauthClientService.lambdaQuery().eq(OauthClient::getClientId, clientId).one();
        if (Objects.nonNull(oauthClient)) {
            BaseClientDetails clientDetails = new BaseClientDetails(
                    oauthClient.getClientId(),
                    oauthClient.getResourceIds(),
                    oauthClient.getScope(),
                    oauthClient.getAuthorizedGrantTypes(),
                    oauthClient.getAuthorities(),
                    oauthClient.getWebServerRedirectUri());
            clientDetails.setClientSecret(PasswordEncoderTypeEnum.NOOP.getPrefix()   oauthClient.getClientSecret());
            clientDetails.setAccessTokenValiditySeconds(oauthClient.getAccessTokenValidity());
            clientDetails.setRefreshTokenValiditySeconds(oauthClient.getRefreshTokenValidity());
            return clientDetails;
        } else {
            throw new NoSuchClientException("没找到客户端信息");
        }
    }
}

创建自定义的userDetails(自定义用户自己的信息)

代码语言:javascript复制
package com.lglbc.oauth2.config.details.user;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @author: 乐哥聊编程(全平台同号)
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SysUserDetails implements UserDetails {

    /**
     * 扩展字段
     */
    private Long userId;

    /**
     * 默认字段
     */
    private String username;
    private String password;
    private Boolean enabled;
    private Collection<SimpleGrantedAuthority> authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

创建根据用户账号获取信息的detailService

代码语言:javascript复制
package com.lglbc.oauth2.config.details.user;

import com.lglbc.oauth2.entity.Role;
import com.lglbc.oauth2.entity.User;
import com.lglbc.oauth2.enums.PasswordEncoderTypeEnum;
import com.lglbc.oauth2.mapper.UserRoleMapper;
import com.lglbc.oauth2.service.RoleService;
import com.lglbc.oauth2.service.UserService;
import com.xiaoleilu.hutool.collection.CollectionUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;


@Service
public class SysUserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userServiceImpl;
    @Autowired
    private RoleService roleServiceImpl;
    @Autowired
    private UserRoleMapper userRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        Map<String, Object> map = new HashMap<>();
        map.put("username", name);
        User user = userServiceImpl.lambdaQuery().eq(User::getUsername, name).one();
        if (user == null) {
            throw new UsernameNotFoundException("此用户不存在");
        }

        Collection<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();
        List<String> roleIds = userRoleMapper.getRoleIdByUserId(user.getId());
        if (CollectionUtil.isNotEmpty(roleIds)){
            List<Role> roles = roleServiceImpl.lambdaQuery().in(Role::getId, roleIds).list();
            List<String> roleCodes = roles.stream().map(Role::getCode).collect(Collectors.toList());
            for (String roleCOde : roleCodes) {
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" roleCOde);
                grantedAuthorities.add(grantedAuthority);
            }
        }

        return new SysUserDetails(user.getId(), user.getUsername(),PasswordEncoderTypeEnum.BCRYPT.getPrefix()   user.getPassword(),true, grantedAuthorities);
    }


}

创建认证服务配置

代码语言:javascript复制
package com.lglbc.oauth2.config;

import com.lglbc.oauth2.config.details.client.ClientDetailsServiceImpl;
import com.lglbc.oauth2.config.details.user.SysUserDetails;
import com.xiaoleilu.hutool.collection.CollectionUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import java.security.KeyPair;
import java.util.*;

/**
 * @author: 乐哥聊编程(全平台同号)
 */
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;
    private final ClientDetailsServiceImpl clientDetailsService;
    // 该对象用来将令牌信息存储到Redis中
    private final RedisConnectionFactory redisConnectionFactory;

    /**
     * OAuth2客户端
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        clients.withClientDetails(clientDetailsService);
    }


    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));

        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
                .tokenServices(tokenServices(endpoints))
        ;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()");
    }

    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);
        return tokenServices;
    }

    /**
     * JWT内容增强
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            Map<String, Object> additionalInfo = CollectionUtil.newHashMap();
            if (Objects.nonNull(authentication.getUserAuthentication())) {
                Object principal = authentication.getUserAuthentication().getPrincipal();
                if (principal instanceof SysUserDetails) {
                    SysUserDetails sysUserDetails = (SysUserDetails) principal;
                    additionalInfo.put("userId", sysUserDetails.getUserId());
                    additionalInfo.put("username", sysUserDetails.getUsername());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
                }
            }
            return accessToken;
        };
    }

    /**
     * 使用非对称加密算法对token签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 密钥库中获取密钥对(公钥 私钥)
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair("jwt", "123456".toCharArray());
        return keyPair;
    }

}

创建安全配置

代码语言:javascript复制
package com.lglbc.oauth2.config;

import com.lglbc.oauth2.common.result.R;
import com.lglbc.oauth2.common.result.ResultCode;
import com.xiaoleilu.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;

/**
 * @author: 乐哥聊编程(全平台同号)
 */
@Configuration
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService sysUserDetailsService;
    private final String realName = ".";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().antMatchers("/oauth/**", "/test/**", "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic().and()
                .csrf().disable()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
    }

    /**
     * 认证管理对象
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 添加自定义认证器
     *
     * @param auth
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    /**
     * 设置默认的用户名密码认证授权提供者
     *
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(sysUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 自定义认证异常响应数据
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            if (e.getMessage().equals("User must be authenticated with Spring Security before authorization can be completed.")) {
                response.addHeader("WWW-Authenticate", "Basic realm= "   realName);
                response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
            } else {
                response.setStatus(HttpStatus.OK.value());
                response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setHeader("Access-Control-Allow-Origin", "*");
                response.setHeader("Cache-Control", "no-cache");
                R result = R.failed(ResultCode.CLIENT_AUTHENTICATION_FAILED);
                response.getWriter().print(JSONUtil.toJsonStr(result));
                response.getWriter().flush();
            }
        };
    }
}

重写token enpoint

代码语言:javascript复制
package com.lglbc.oauth2;

import com.lglbc.oauth2.common.result.R;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;

import java.security.KeyPair;
import java.security.Principal;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;

/**
 * @author: 乐哥聊编程(全平台同号)
 */
@RestController
@RequestMapping("/oauth")
@AllArgsConstructor
@Slf4j
public class AuthController {

    private KeyPair keyPair;
    private final TokenEndpoint tokenEndpoint;

    @PostMapping("/token")
    public Object postAccessToken(
            Principal principal,
            @RequestParam Map<String, String> parameters
    ) throws HttpRequestMethodNotSupportedException {
        OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        return R.ok(accessToken);
    }

    @GetMapping("/public-key")
    public Map<String, Object> getPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }
}

演示四种授权模式

授权码模式

获取授权码
  • 浏览器打开:http://localhost:8080/oauth/authorize?response_type=code&client_id=admin
  • 输入用户名和密码

在这里插入图片描述

  • 点击授权,获取授权码
获取token
代码语言:javascript复制
curl --location --request POST --X POST 'http://localhost:8080/oauth/token?code=k9jcxv&grant_type=authorization_code' 
--header 'User-Agent: Apipost client Runtime/ https://www.apipost.cn/' 
--header 'Authorization: Basic YWRtaW46YW1z'
刷新token
代码语言:javascript复制
curl --location --request POST --X POST 'http://localhost:8080/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhbXMiLCJzY29wZSI6WyJ3cml0ZSJdLCJhdGkiOiI4NjZmYWY2MC05MTE1LTQ0NGMtOWNkNi05MjRmYTRlZjQ4YjUiLCJleHAiOjE2NTQ1MzExMzcsInVzZXJJZCI6NCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiIyNDg1M2NiNi02ZGM1LTRlMDQtYjAxOC0wMGE0MGMxNGMyNjAiLCJjbGllbnRfaWQiOiJhZG1pbiIsInVzZXJuYW1lIjoiYW1zIn0.BX87xbC1SEVgXsQ6qdI1_RLVOY1ffqoKDvd-Fnu5FfJor6CR-3NtUjMBIQig_jAnJwptG8ZIs1q715GAQTtgoRVUMsnXBv8RKmlDvxjclUYIWrRXy1d9Rj84WC4niDU8rOzazr4L1-chkwod3rAEDAZSiBcd4XZ1S2cfG7WBSH1TcivNtY0pk_ePS08ItN_2wYm57-7_JD8XCRecpn6nF6ax20ysimqPftAKIiVzvhhN4O_TsHd3-lB7ZYTsT-SlzjBu43gm5eGFWhsOBK7NgxObwoeXaoARrzW3GaUw8DsLTKsDofyIIFGnKee9sooAwejPrsxtVRJlfj8gIc5EdA&grant_type=refresh_token' 
--header 'User-Agent: Apipost client Runtime/ https://www.apipost.cn/' 
--header 'Authorization: Basic YWRtaW46YW1z'

简单模式

  • 浏览器访问:http://localhost:8080/oauth/authorize?response_type=token&client_id=admin
  • 输入用户名和密码(用户信息,不是客户端)
  • 点击授权,直接获取token

用户名密码模式

代码语言:javascript复制
curl --location --request POST --X POST 'http://localhost:8080/oauth/token?grant_type=password&username=ams&password=ams' 
--header 'User-Agent: Apipost client Runtime/ https://www.apipost.cn/' 
--header 'Authorization: Basic YWRtaW46YW1z'

客户端模式

代码语言:javascript复制
curl --location --request POST --X POST 'http://127.0.0.1:8080/oauth/token?grant_type=client_credentials' 
--header 'User-Agent: Apipost client Runtime/ https://www.apipost.cn/' 
--header 'Authorization: Basic YWRtaW46YW1z'

创建资源服务

资源服务配置

代码语言:javascript复制
package com.lglbc.oauth2.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lglbc.oauth2.common.KeyPairUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
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;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.client.RestTemplate;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
public class Oauth2ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    private static final String CHECK_TOKEN_URL = "http://localhost:8080/oauth/check_token";
    // 也可以使用认证服务去校验
//    @Override
//    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//        RemoteTokenServices tokenService = new RemoteTokenServices();
//        tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
//        tokenService.setClientId("resource");
//        tokenService.setClientSecret("resource");
//        resources.tokenServices(tokenService);
//    }
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //设置用于解码的非对称加密的公钥
        converter.setVerifierKey(KeyPairUtil.getPubKey());
        return converter;
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 任何请求都必须具有all这个域的授权
                .antMatchers("/test/read").access("#oauth2.hasScope('read') || #oauth2.hasScope('write')")
                .antMatchers("/test/write").access("#oauth2.hasScope('write')")
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

创建安全配置,添加拦截

代码语言:javascript复制
package com.lglbc.oauth2.config;

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;

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/test/**").authenticated();
        // 禁用CSRF
        http.csrf().disable();
    }
}

演示资源服务

定义两个接口

  • /read:需要有admin角色 并且有对目前服务的读权限
代码语言:javascript复制
    @RequestMapping("/read")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String read(){
        System.out.println("read");
        return "read";
    }
  • /write:需要有admin角色 并且有对目前服务的读写权限
代码语言:javascript复制
    @RequestMapping("/write")
//    @Secured({"ROLE_ADMIN"})
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String write() throws ParseException {
        return UserContextUtil.getUserId() "" UserContextUtil.getUserName();
    }

常见问题

角色权限注解问题

  • 不加ROLE_前缀,只能使用此注解

@PreAuthorize("hasAuthority('ADMIN2')")

  • 加ROLE_前缀,可以使用如下注解:

@PreAuthorize("hasRole('ADMIN')") //允许

@PreAuthorize("hasRole('ROLE_ADMIN')") //允许

@PreAuthorize("hasAuthority('ROLE_ADMIN')") //允许

客户端密钥问题

  • 如果密钥没有加密,在构建clientDetail的时候一定要加上{noop}前缀
  • 否则加上 {bcrypt}前缀

持续整理...

获取用户信息

源码部分

由于在资源服务中,我们没有重写userDetailService,所以我们是获取不到用户的信息(我们也没必要写),但是我们可以自己解析token,拿到用户信息

代码语言:javascript复制
package com.lglbc.oauth2.config;

import com.lglbc.oauth2.common.UserContextUtil;
import com.nimbusds.jose.JWSObject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

public class UserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        if (StringUtils.isBlank(authorization)) {
            return true;
        }
        String token = StringUtils.substringAfter(authorization, "Bearer ");
        if (StringUtils.isBlank(token)) {
            return true;
        }
        try {
            Map<String, Object> stringObjectMap = JWSObject.parse(token).getPayload().toJSONObject();
            UserContextUtil.setUser(stringObjectMap);
        } catch (Exception e) {

        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContextUtil.removeUser();
    }
}

成长心路 | 优质书单 | 面试资料

牛人故事 | 前沿技术 | 视频教程

0 人点赞