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));
至于相关细节,以后如果有空,我再继续补充。
评论