Spring Security OAuth2 密码模式
我的应用,主要五个类
- 继承AuthorizationServerConfigurerAdapter,主要功能
- 注入authenticationManager开启密码验证模式(默认不开启)
- 设置tokenStore保存token,框架提供了jdbc、inmemory、redis存储方式,此处使用inmemory内存模式,实际保存在map里
- 注入userDetailsService设置用作验证比较的用户对象,详见下面
- clients设置内存保存客户端,设置客户端id和secret,设置允许的认证类型,设置授权角色,设置token有效期等
- endpoints设置认证管理器,设置token存储,设置加载保存的用户service
- 注入自己实现的PasswrodEncoder,详见下面
import com.my.service.UserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailServiceImpl userDetailService;
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore()).userDetailsService(userDetailService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("my-client-with-secret")
.authorizedGrantTypes("password", "refresh_token")
.authorities("ROLE_CLIENT")
// .scopes("trust")
.resourceIds("oauth2-resource")
.accessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1))
.refreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30))
.secret("secret");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new RSAPasswordBCryptEncoder();
}
}
- 实现UserDetailsService接口,获取保存的用户用做和输入的用户密码比较
import com.my.model.AppUserAccount;
import com.my.service.UserDetailService;
import org.nutz.dao.Cnd;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class UserDetailServiceImpl implements UserDetailService {
@Autowired
private AppUserAccountServiceImpl appUserAccountServiceImpl;
@Override
public UserDetails loadUserByUsername(String userAccount) throws UsernameNotFoundException {
AppUserAccount appUserAccount = appUserAccountServiceImpl.fetch(Cnd.where("user_account", "=", userAccount));
if (appUserAccount != null) {
User user = new User(userAccount, appUserAccount.getUserPassword(), true, true, true, true, Collections.EMPTY_SET);
return user;
} else {
throw new UsernameNotFoundException("Could not find the user '" + userAccount + "'");
}
}
}
- 继承ResourceServerConfigureAdapter,设置资源访问控制
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;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/appuser/hello**/**").permitAll()
.anyRequest().authenticated();
}
}
- 继承GlobalAuthenticationConfigurerAdapter,全局安全配置我还没有用到
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
@Configuration
public class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
}
- 自定义的PasswordEncoder,解析出用户输入的明文密码和数据库的密文密码比较,这里偷懒直接调用了springsecurity提供的BCrypt哈希类
import com.wellcommsoft.util.RSA;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
public class RSAPasswordBCryptEncoder implements PasswordEncoder {
public class RSAPasswordBCryptEncoder implements PasswordEncoder {
/**
* @param rawPassword 客户端发送的密码,一般加密的
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/**
* @param rawPassword 客户端传的RSA公钥加密的密码
* @param encodedPassword 数据库密文密码
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String plainPassword = RSA.decryptPrivateKey(rawPassword.toString());
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder.matches(plainPassword, encodedPassword);
}
}
测试
-
默认获取token的url在TokenEndPoint类里提供的/oauth/token,获取token方式如下
-
上图TYPE选择Basic Auth,Username和Password填写上面client设置的withClient和secret作为认证客户端的clientid和secret
-
上图中Headers里的Authorization的值实际是填写的username和password的base64编码值,即
Authorization = Basic+空格+base64(username-password)
-
body里填写的scope和grant_type同样与client设置的一致,grant_type填写password,都不能为空。这里的用户密码是实际的用户账号和经过客户端加密的用户密码
-
返回的内容包括访问限制资源的token,token类型,刷新token,访问token有效时间,授权范围(注意:如果client.authorizedGrantTypes("password", "refresh_token")没有设置refresh_token则返回的内容不包含refresh_token)
-
访问限制资源时候在Headers里加上Anthorization值为token_type+空格+access_token
- 刷新token时Authorization和Headers里和获取token一样填写client设置的clientid和secret,body里区别是grant_type类型填写refresh_token。返回结果和获取token一致,注意access_token和refresh_token都返回的新值。
controller里获取用户账号
String username = request.getUserPrincipal().getName();
退出登录
@Autowired
private ConsumerTokenServices consumerTokenServices;
@DeleteMapping("/logout")
public Result logout() {
String authorization = request.getHeader("Authorization");
if (!StringUtil.strIsNullOrEmpty(authorization)) {
String accessToken = authorization.split(" ")[1];
boolean removeToken = consumerTokenServices.revokeToken(accessToken);
if (removeToken) {
return ResultUtils.success();
}
}
return ResultUtils.error();
}