java

spring boot 集成 oauth2

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

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

1、添加依赖,在 pom.xml 里加入:

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

2、在 pom.xml 里的 </dependencies> 下面添加:

<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>

3、修改 pom.xml 里的 properties 节点,添加:

<spring-cloud.version>Hoxton.SR4</spring-cloud.version>

修改后的 pom.xml 文件大致如下:

<?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 等几个类,限于篇幅,本文将只有核心代码,其他细节日后再补充。

1、CustomClientDetailsService.java

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;
    }
}

2、CustomUserDetailsService.java

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);
    }
}

3、RedisTokenConfig.java

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;
    }
}

4、ResourceServer.java

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);
    }
}

5、SpringSecurityConfig.java

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

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);
}

2、LoginServiceImpl.java

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);
    }
}

3、SecurityUtils.java

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);
    }
}

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

@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));

至于相关细节,以后如果有空,我再继续补充。

full-stack-trip

Share
Published by
full-stack-trip

Recent Posts

Android 自定义 View 入门

说来惭愧,工作数年,连基本的自…

4 年 ago

retrofit 同时支持 xml 和 json

retrofit 解析 jso…

4 年 ago

mysql - 存储过程 从入门到放弃

最近有个报表的需求,于是乎用了…

4 年 ago

奶嘴战略 - 你不得不知道的扎心真相(一)

一句:英雄枯骨无人问,戏子家事…

4 年 ago

acme.sh 的简单使用

acme.sh 是纯 shel…

4 年 ago

wrk -更现代化的http压测工具

wrk 是一款更现代化的 ht…

4 年 ago