美文网首页SpringBoot学习
spring-security-oauth2 流程详解

spring-security-oauth2 流程详解

作者: 馒Care | 来源:发表于2021-06-17 00:47 被阅读0次

关于OAuth2的解释太少了,这玩意儿困扰我太久了

1.这玩意儿是什么
这玩意儿,不好理解,特别是我这种android开发,接触这个,我觉得太难了,太绕了,我的理解更多的是,既然要做校验,为什么不直接写拦截器呢?拦截器不是更加简单易懂吗?

2.这玩意儿怎么运转的
这个真的不好解释,假设,我有一个登录服务,暂且称LoginService

  • LoginService 端口90000
    @PostMapping("/login")
    @ApiOperation(value = "后台管理员登录")
    @ApiImplicitParams(
            {
                    @ApiImplicitParam(name = "loginParam", value = " 登录参数")
            }
    )
    public Result<LoginMemberVO> login(@RequestBody LoginParam loginParam) {
        return Result.buildSuccess(memberLoginService.login(loginParam));
    }
  • 我模拟登录的时候,就可以使用
http://localhost:90000/login
  • 然后就是账号密码一顿输入,这个时候,密码怎么破?都是明文啊,数据库保存也都是明文啊,于是乎,spring-security-oauth2登场

2.首先引库

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

3.然后自定义OAuth2校验,这个地方卡了我3天啊!这里建议用Feign调用

  //调用远程auth服务获取token
@FeignClient(value = "zero-authentication")
public interface OAuth2FeignClient {

    @PostMapping("/oauth/token")
    ResponseEntity<JwtToken> getToken(
            @RequestParam("grant_type") String grantType , // 授权类型
            @RequestParam("username") String username , // 用户名
            @RequestParam("password") String  password , // 用户的密码
            @RequestParam("LOGIN_TYPE")  String loginType,  // 登录的类型
            @RequestHeader("Authorization") String basicToken // Basic 由第三方客户端信息加密出现的值
    ) ;
}

4.在具体登录的地方进行oauth2的调用

public LoginMemberVO login(LoginParam loginParam) {
        LoginMemberVO loginMemberVO = null;

        //调用远程auth服务获取token
        ResponseEntity<JwtToken> tokenResponseEntity = oAuth2FeignClient.getToken("password", loginParam.getUsername(), loginParam.getPassword(), "MEMBER_TYPE", basicToken);
        if (tokenResponseEntity.getStatusCode() == HttpStatus.OK) {
            JwtToken jwtToken = tokenResponseEntity.getBody();
            if(jwtToken == null) {
                ExceptionCatcher.catchAuthFailEx();
            }
            //构建返回结果
            loginMemberVO = new LoginMemberVO(loginParam.getUsername(), jwtToken.getExpiresIn(), jwtToken.getTokenType() + " " + jwtToken.getAccessToken(), jwtToken.getRefreshToken());

            //保存token到redis
            redisUtils.setEx(RedisPrefixConstants.JWT_MEMBER_PREFIX + jwtToken.getAccessToken(), "", jwtToken.getExpiresIn(), TimeUnit.SECONDS);
        }
        return loginMemberVO;
    }

5.请注意以下代码

ResponseEntity<JwtToken> tokenResponseEntity = oAuth2FeignClient.getToken("password", loginParam.getUsername(), loginParam.getPassword(), "MEMBER_TYPE", basicToken);
  • 这句是干嘛的?为啥就能返回数据了?就是这里卡了我3天,我完全蒙蔽,这就一个Feign的远程调用,我似乎找不到哪里定义了这么一个接口。
  • 看源码TokenEndpoint
@RequestMapping(
        value = {"/oauth/token"},
        method = {RequestMethod.GET}
    )
    public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!this.allowedRequestMethods.contains(HttpMethod.GET)) {
            throw new HttpRequestMethodNotSupportedException("GET");
        } else {
            return this.postAccessToken(principal, parameters);
        }
    }

    @RequestMapping(
        value = {"/oauth/token"},
        method = {RequestMethod.POST}
    )
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        } else {
            String clientId = this.getClientId(principal);
            ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
            if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
                throw new InvalidClientException("Given client ID does not match authenticated client");
            } else {
                if (authenticatedClient != null) {
                    this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
                }

                if (!StringUtils.hasText(tokenRequest.getGrantType())) {
                    throw new InvalidRequestException("Missing grant type");
                } else if (tokenRequest.getGrantType().equals("implicit")) {
                    throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
                } else {
                    if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
                        this.logger.debug("Clearing scope of incoming token request");
                        tokenRequest.setScope(Collections.emptySet());
                    }

                    if (this.isRefreshTokenRequest(parameters)) {
                        tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
                    }

                    OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
                    if (token == null) {
                        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
                    } else {
                        return this.getResponse(token);
                    }
                }
            }
        }
    }

  • 没错,就是这里了,这里定义了这个oauth的调用,我们继续看源码
  • 到这里,我们带着问题

1.首先,我如何登陆,我输入的账号密码,oauth2,做了什么进行匹配呢?

这里可以肯定的是,远程服务端的数据库保存的密码,绝对不会是明文的,所以,必然有某种加密规则把远程的数据,跟oauth2进行对比。

以下流程十分重要,一定要认真对着源码看下去

  • TokenEndpoint类源码的这一句,可以看出是进行token的获取
 OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

  • 会调用CompositeTokenGranter的grant方法

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        Iterator var3 = this.tokenGranters.iterator();

        OAuth2AccessToken grant;
        do {
            if (!var3.hasNext()) {
                return null;
            }

            TokenGranter granter = (TokenGranter)var3.next();
           //主要看这句了
            grant = granter.grant(grantType, tokenRequest);
        } while(grant == null);

        return grant;
    }
  • AbstractTokenGranter 接着调用grant
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        if (!this.grantType.equals(grantType)) {
            return null;
        } else {
            String clientId = tokenRequest.getClientId();
            ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId);
            this.validateGrantType(grantType, client);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Getting access token for: " + clientId);
            }

            return this.getAccessToken(client, tokenRequest);
        }
    }

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
    }
  • ResourceOwnerPasswordTokenGranter的方法中getOAuth2Authentication
userAuth = this.authenticationManager.authenticate(userAuth);
  • 走到ProviderManager的authenticate(Authentication authentication)方法体
result = provider.authenticate(authentication);
  • 又走到 AbstractUserDetailsAuthenticationProvider的authenticate方法体
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
  • 又又又走到DaoAuthenticationProvider retrieveUser中
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  • 最终走到loadUserByUsername
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

关键的地方来了,走了这么一大圈,为了做毛???就是为了,获取我们的密码,然后进行加密,最后跟远程数据库的加密后的密码进行对比

  • 所以,我们一般会实现UserDetailsService,进行各种输出
@Service
public class CustomizeUserDetailsService implements UserDetailsService {

    @Autowired
    private SysUserRepository sysUserRepository;

    @Autowired
    private SysPrivilegeRepository sysPrivilegeRepository;

    @Autowired
    private MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if(requestAttributes == null) {
            throw new AuthenticationServiceException(LoginConstant.LOGIN_TYPE + "参数不存在");
        }

        String loginType = requestAttributes.getRequest().getParameter(LoginConstant.LOGIN_TYPE);
        if(StringUtils.isBlank(loginType)) {
            throw new AuthenticationServiceException(LoginConstant.LOGIN_TYPE + "参数不存在");
        }

        UserDetails userDetails = null;
        switch (loginType) {
            case LoginConstant.ADMIN_TYPE:
                userDetails = loadAdminByUsername(username);
                break;
            case LoginConstant.MEMBER_TYPE:
                userDetails = loadMemberByUsername(username);
                break;
            default:
                throw new AuthenticationServiceException("暂不支持的登录方式: " + loginType);
        }
        return userDetails;
    }

    private UserDetails loadMemberByUsername(String username) {
        Member member = memberRepository.findByUsernameOrPhoneOrEmailAndStatus(username, username, username, 1);
        return new User(
                String.valueOf(member.getId()),
                member.getPassword(),
                true,
                true,
                true,
                true,
                Arrays.asList(new SimpleGrantedAuthority("ROLE_MEMBER"))
        );
    }

    private UserDetails loadAdminByUsername(String username) {
        SysUser sysUser = sysUserRepository.findByUsernameAndStatus(username, 1);
        if(ObjectUtils.isNotEmpty(sysUser)) {
            Collection authorities;
            User user = new User(String.valueOf(sysUser.getId()),
                    sysUser.getPassword(),
                    true,
                    true,
                    true,
                    true,
                    getSysUserPermissions(sysUser.getId()));

            return user;
        }
        return null;
    }

    /**
     * 通過用户ID获取用户权限
     * @param userId
     * @return
     */
    private Set<SimpleGrantedAuthority> getSysUserPermissions(Long userId) {
        String roleCode = sysUserRepository.getRoleCode(userId);

        List<SysPrivilege> privileges;

        if(ADMIN_ROLE_CODE.equals(roleCode)) {
            privileges = sysPrivilegeRepository.findAll();
        }else {
            privileges = sysPrivilegeRepository.findByUserId(userId);
        }

        if(ObjectUtils.isEmpty(privileges)) {
            return Collections.emptySet();
        }

        return privileges
                .stream()
                .distinct()
                .map( privilege -> new SimpleGrantedAuthority(privilege.getName()))
                .collect(Collectors.toSet());
    }
}

到这里一切似乎十分的美好,然后哪里做密码的校验呢???oauth2 是真的绕,它提供了几种加密的方案,是可以配置的,所以配置在哪里呢??这个时候
WebSecurityConfigurerAdapter登场了,我们需要继承WebSecurityConfigurerAdapter进行自定义了

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); //关闭csrf
        http.authorizeRequests().anyRequest().authenticated();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
  • BCryptPasswordEncoder 这个玩意儿就是我们配置的加密玩意儿,通过源码可知,里面有encode跟match方法
public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }
public String encode(CharSequence rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            String salt;
            if (this.random != null) {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
            }

            return BCrypt.hashpw(rawPassword.toString(), salt);
        }
    }
  • 简单理解,就是,我传入了密码(admin),然后通过encode加密后,获取一串字符串,这个字符串再根据match,把远程的密码,跟encode后的字符串进行对比判断密码是否正确

那么问题又来了,什么时候encode呢?AuthorizationServerConfigurerAdapter这个玩意儿就是做这些配置的初始化的

@EnableAuthorizationServer
@Configuration
public class AuthenticationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Qualifier("customizeUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("zero-mall")
                .secret(passwordEncoder.encode("zero-mall-secret"))
                .scopes("all")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(24 * 7200)
                .refreshTokenValiditySeconds(7 * 24 * 7200)
                .and()
                .withClient("inside-app")
                .secret(passwordEncoder.encode("inside-secret"))
                .authorizedGrantTypes("client_credentials")
                .scopes("all")
                .accessTokenValiditySeconds(7 * 24 *3600);
        super.configure(clients);
    }


    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtAccessTokenConverter());
        super.configure(endpoints);
    }

    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
        ClassPathResource classPathResource = new ClassPathResource("zero-mall.jks");
        // 获取KeyStoreFactory
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, "123456".toCharArray()) ;
        tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("zero-mall", "123456".toCharArray()));
        return tokenConverter;
    }
}
  • 不用太在意里面写了什么,关键是流程,贴出来的代码都可以自定义
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("zero-mall")
                .secret(passwordEncoder.encode("zero-mall-secret"))
                .scopes("all")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(24 * 7200)
                .refreshTokenValiditySeconds(7 * 24 * 7200)
                .and()
                .withClient("inside-app")
                .secret(passwordEncoder.encode("inside-secret"))
                .authorizedGrantTypes("client_credentials")
                .scopes("all")
                .accessTokenValiditySeconds(7 * 24 *3600);
        super.configure(clients);
    }
  • 这里初始化了 .secret(passwordEncoder.encode("zero-mall-secret"))配置,然后就是一顿操作了

最后,其实这里还有一个类 ResourceServerConfigurerAdapter,我暂时没有用到,但是通过源码我们可以知道,也是配置

public void configure(HttpSecurity http) throws Exception {
        ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
    }
  • 这个配置告诉我们,所有的请求都需要经过oauth2的认证,但是这一步,一般我们都是放网关层(getway)处理掉了

总结一波,oauth2太绕了。基本把我绕晕了,实在蛋疼,我查阅了太多资料,基本对我这个初学者一点用都没有,所以自己记录一把。后续,把getway网关层也描述一下

相关文章

网友评论

    本文标题:spring-security-oauth2 流程详解

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