spring boot 集成 oauth2

#spring-boot

2025-02-23 14:11:19

spring boot 集成 oauth2 需要添加 spring-cloud-starter-oauth2 依赖, 另外,还需要指定 spring cloud 的版本或者指定 spring-cloud-starter-oauth2 的版本,但是后者不是推荐的做法。假设你依赖了好几个 spring cloud 组件,而你却每个组件单独指定了版本,不是指定 spring cloud 的版本,则可能出现错误,依赖上的混乱。

spring boot 集成 oauth 可以按如下步骤进行

添加 oauth2 的依赖

  1. 在 pom.xml 里添加
1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
  1. 在 pom.xml 里的 下面添加:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  1. 修改 pom.xml 里的 properties 节点,添加:
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>

修改后的 pom.xml 应该是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>top.kpromise</groupId>
    <artifactId>xxxx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>netty</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
    </properties>

    <dependencies>
    
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

这里,我删除了其他无关的配置。现在,我们开始敲代码啦,核心代码主要涉及:ClientDetailsService、UserDetailsService、TokenStore、DefaultTokenServices、ResourceServerConfigurerAdapter、WebSecurityConfigurerAdapter 等几个类,限于篇幅,本文将只有核心代码。

spring boot oauth2 核心类

  1. CustomClientDetailsService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Component;
import top.kpromise.note.data.Config;

import java.util.ArrayList;

@Component
public class CustomClientDetailsService implements ClientDetailsService {

    @Override
    public ClientDetails loadClientByClientId(String clientId) {

        BaseClientDetails baseClientDetails = new BaseClientDetails(Config.clientId, null,
                Config.scope, Config.grantType, null, Config.redirectUri);
        baseClientDetails.setClientSecret(Config.clientSecret);

        baseClientDetails.setRefreshTokenValiditySeconds(Config.refreshTokenValiditySeconds);
        baseClientDetails.setAccessTokenValiditySeconds(Config.accessTokenValiditySeconds);

        baseClientDetails.setAutoApproveScopes(new ArrayList<String>() {{
            add(Config.scope);
        }});

        return baseClientDetails;
    }
}
  1. CustomUserDetailsService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.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.stereotype.Component;
import top.kpromise.note.modules.user.entity.UserEntity;
import top.kpromise.note.modules.user.service.UserService;

import java.util.ArrayList;
import java.util.List;

@Component
public class CustomUserDetailsService implements UserDetailsService {

    private final UserService userService;

    public CustomUserDetailsService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {

        UserEntity userEntity = userService.findByUserName(userName);
        if (userEntity == null) throw new UsernameNotFoundException("userName " + userName + " not found");

        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        return new User(userName, userEntity.getPassword(), true,
                true, true,
                true, grantedAuthorities);
    }
}
  1. RedisTokenConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

@Configuration
public class RedisTokenConfig {

    @Bean("tokenStore")
    public TokenStore tokenStore(RedisConnectionFactory factory) {
        return new RedisTokenStore(factory);
    }

    @Bean("tokenServices")
    @Primary
    public DefaultTokenServices tokenServices(TokenStore tokenStore) {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setReuseRefreshToken(false);
        return defaultTokenServices;
    }
}
  1. ResourceServer.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
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.TokenStore;

import javax.annotation.Resource;

@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Resource
    private TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();
        http.headers().frameOptions().disable();

        String[] whiteList = {"/user/**", "/oauth2/**", "/api/**", "/services/**", "/health", "/druid/**"};

        http.requestMatchers().antMatchers(whiteList)
                .and()
                .authorizeRequests()
                .antMatchers(whiteList)
                .permitAll();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
}
  1. SpringSecurityConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 javax.annotation.Resource;

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll();
    }

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}
  1. LoginService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import top.kpromise.common.base.Result;
import top.kpromise.note.modules.user.entity.UserEntity;
import top.kpromise.note.modules.user.model.LoginResult;

public interface LoginService {

    Result<LoginResult> login(UserEntity user);

    void logout(String userName);

    Result<LoginResult> refreshToken(String refreshToken);
}
  1. LoginServiceImpl.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Service;
import top.kpromise.common.base.Result;
import top.kpromise.common.utils.SecurityUtils;
import top.kpromise.note.config.CustomClientDetailsService;
import top.kpromise.note.data.Config;
import top.kpromise.note.modules.user.entity.UserEntity;
import top.kpromise.note.modules.user.model.LoginResult;
import top.kpromise.note.modules.user.service.LoginService;
import top.kpromise.note.modules.user.service.UserService;

import java.util.Collection;
import java.util.HashMap;

@Service
@Slf4j
public class LoginServiceImpl implements LoginService {

    private final CustomClientDetailsService customClientDetailsService;
    private final AuthenticationManager authenticationManager;
    private final DefaultTokenServices tokenServices;
    private final TokenStore tokenStore;
    private final UserService userService;
    private final AuthorizationServerTokenServices defaultAuthorizationServerTokenServices;

    public LoginServiceImpl(CustomClientDetailsService customClientDetailsService,
                            AuthenticationManager authenticationManager, DefaultTokenServices tokenServices,
                            TokenStore tokenStore, UserService userService,
                            AuthorizationServerTokenServices defaultAuthorizationServerTokenServices) {
        this.customClientDetailsService = customClientDetailsService;
        this.authenticationManager = authenticationManager;
        this.tokenServices = tokenServices;
        this.tokenStore = tokenStore;
        this.userService = userService;
        this.defaultAuthorizationServerTokenServices = defaultAuthorizationServerTokenServices;
    }

    @Override
    public Result<LoginResult> login(UserEntity user) {
        if (user == null || user.getUserName() == null) {
            return Result.error("请输入用户名");
        }
        if (user.getPassword() == null) {
            return Result.error("请输入密码");
        }
        UserEntity loginUser = userService.findByUserName(user.getUserName());
        if (loginUser == null) {
            return Result.error("帐户不存在");
        }
        if (loginUser.getUserState() == Config.userStateLock) {
            return Result.error("账号已锁定,请联系管理员");
        }
        if (loginUser.getUserState() == Config.userStateCancel) {
            return Result.error("账号已注销,请联系管理员");
        }
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = getNewOauthTokenByPassword(loginUser.getUserName(), user.getPassword());
        } catch (Exception e) {
            return Result.error("登录失败,密码错误");
        }
        if (oAuth2AccessToken == null) {
            return Result.error("登录失败,系统异常");
        }
        LoginResult loginResult = new LoginResult();
        loginResult.fromToken(oAuth2AccessToken);
        return Result.data(loginResult);
    }

    private OAuth2AccessToken getNewOauthTokenByPassword(String userAccount, String userPassWord) {

        logout(userAccount);

        Authentication usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userAccount,
                SecurityUtils.md5(userAccount, userPassWord));
        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(buildLoginRequest(), authentication);
        oAuth2Authentication.setAuthenticated(true);
        return defaultAuthorizationServerTokenServices.createAccessToken(oAuth2Authentication);
    }

    private OAuth2Request buildLoginRequest() {
        String clientId = Config.clientId;
        ClientDetails clientDetails = customClientDetailsService.loadClientByClientId(clientId);
        return new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "password")
                .createOAuth2Request(clientDetails);
    }

    private TokenRequest buildRefreshTokenRequest() {
        String clientId = Config.clientId;
        ClientDetails clientDetails = customClientDetailsService.loadClientByClientId(clientId);
        return new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "refresh_token");
    }

    @Override
    public void logout(String userName) {
        Collection<OAuth2AccessToken> list = tokenStore.findTokensByClientIdAndUserName(Config.clientId, userName);
        if (list.isEmpty()) return;
        for (OAuth2AccessToken token : list) {
            log.debug("revokeToken for {} and token is {}", userName, token.getValue());
            tokenServices.revokeToken(token.getValue());
        }
    }

    @Override
    public Result<LoginResult> refreshToken(String refreshToken) {
        log.debug("refreshToken for {}", refreshToken);
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenServices.refreshAccessToken(refreshToken, buildRefreshTokenRequest());
        } catch (Exception e) {
            return Result.error("登录失败,密码错误");
        }
        if (oAuth2AccessToken == null) {
            return Result.error("登录失败,系统异常");
        }
        LoginResult loginResult = new LoginResult();
        loginResult.fromToken(oAuth2AccessToken);
        return Result.data(loginResult);
    }
}
  1. SecurityUtils.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.util.DigestUtils;

public class SecurityUtils {

    public static String md5(String text, String key) {
        return DigestUtils.md5DigestAsHex((text + key).getBytes());
    }

    public static String password(String userName, String password) {
        String md5Password = md5(userName, password);
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder.encode(md5Password);
    }

    public static String encodePassword(String md5Password) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder.encode(md5Password);
    }

    public static boolean checkPassword(String rawPassword, String encodedPassword) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        return bCryptPasswordEncoder.matches(rawPassword, encodedPassword);
    }
}

最后,再贴一段创建用户的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RequestMapping(value = "/createAccount", method = RequestMethod.PUT)
@ApiOperation(value = "创建账号")
@ApiImplicitParam(name = "loginUser", dataType = "LoginUser",
        paramType = "body", required = true)
public Result<String> createAccount(@RequestBody LoginUser loginUser) throws Exception {
    ValidationUtils.throwIfValidateFailed(loginUser);
    UserEntity userEntity = new UserEntity();
    userEntity.setUserName(loginUser.getUserName());
    userEntity.setNickName("十三");
    userEntity.setPassword(SecurityUtils.password(loginUser.getUserName(), loginUser.getPassword()));
    userEntity.preSave();
    userService.save(userEntity);
    return Result.success("用户创建成功,请登录");
}

请注意,这里,用户密码是 先 md5 之后又 使用了 Bcrypt 加密,所以,创建用户时,密码设置以及登录时密码加密都需要特别注意,对应的分别是创建用户这段代码里的:

userEntity.setPassword(SecurityUtils.password(loginUser.getUserName(), loginUser.getPassword()));

以及 LoginServiceImpl.java 里 getNewOauthTokenByPassword 方法中的:

Authentication usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userAccount, SecurityUtils.md5(userAccount, userPassWord));
最后更新于