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