公司项目是基于dubbo和zookeeper在门户项目端提供接口统一鉴权的,一直没有接触过spring security。出于好奇一直想了解下spring security怎么使用以及oauth2和jwt的鉴权过程,这次学习过程中将spring security搭上oauth2和jwt做一个示例程序。
OAuth 2.0
OAuth 2.0是一套关于授权的开放标准,主要针对第三方应用认证授权场景,详细信息可参考RFC 6749。
OAuth 2.0定义了四个角色:
- resource owner:资源所有者,即用户本身
- resource server:资源服务器,服务提供商存放用户应用资源的服务器
- client:客户端,即第三方应用
- authorization server:授权服务器,服务提供商专门处理认证授权的服务器
打个比方,我打开简书发现没有账号但又懒得注册,点击使用QQ账号登录,那么这里的我就是resource owner,简书是client,QQ保存账号信息比如头像的服务器就是resource server,QQ用来处理登录授权的服务器就是authorization server,如果以上操作按照OAuth 2.0的标准,它的流程大致如下:
其中关键就是授权码token的获取,对于token的获取OAuth 2.0定义了四种方式:
- Authorization Code:授权码模式,例如微信上给第三方小程序授权
- Implicit:简化模式,授权码模式的简化版本,嫌授权码模式麻烦时用
- Resource Owner Password Credentials:资源所有者密码凭证模式,一般高信任的内部应用间使用
- Client Credentials:客户端凭证模式,一般用于开放API调用
JWT
即JSON Web Token的缩写,json格式的token,包含三部分:
- Header:头部,包含类型和签名算法名称
- Payload:载荷,主要内容,包含需要传递的信息
- Signature:签名,对头部和载荷哈希后的内容
使用JWT主要是服务器端不需要存储token,资源服务器也不需要访问授权服务器验证token,每次访问只需对应资源服务器验证token有效性,减少服务器开销。
Spring security
Spring security框架基于OAuth 2.0做了相关实现,我们只需要做相应的配置就能实现一个基于OAuth 2.0和JWT的授权服务。
引入相关依赖
这里是使用gradle做依赖管理
dependencies {
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.1.3.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '2.1.3.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.1.3.RELEASE'
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-security', version: '2.1.1.RELEASE'
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-oauth2', version: '2.1.1.RELEASE'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.8.1'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
runtime group: 'com.alibaba', name: 'druid', version: '1.1.13'
runtime group: 'mysql', name: 'mysql-connector-java', version: '5.1.47'
}
配置spring security
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService authUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/oauth/**").permitAll();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
配置授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
@Qualifier("jwtTokenEnhancer")
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单认证
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client_admin")
.resourceIds("resource")
.authorizedGrantTypes("password", "client_credentials", "refresh_token")
.scopes("read")
.authorities("admin")
.secret(passwordEncoder.encode("123456"))
.accessTokenValiditySeconds(3 * 60 * 60)
.refreshTokenValiditySeconds(6 * 60 * 60);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(jwtTokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain)
.exceptionTranslator(loggingExceptionTranslator());
}
@Bean
public WebResponseExceptionTranslator loggingExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
private Log log = LogFactory.getLog(getClass());
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
//异常堆栈信息输出
log.error("异常堆栈信息", e);
return super.translate(e);
}
};
}
}
配置资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices resourceJwtTokenServices;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
//resource资源只允许基于令牌的身份验证
resources.resourceId("resource").stateless(true);
resources.tokenServices(resourceJwtTokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
// 由于我们希望在用户界面中访问受保护的资源,因此我们需要允许创建会话
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
//启用匿名登录,不可访问受保护资源
.anonymous()
.and()
.authorizeRequests()
//配置protected访问控制,必须认证过后才可以访问
.antMatchers("/protected/**").authenticated();
}
}
JWT配置
@Configuration
public class JwtTokenConfig {
private static KeyPair KEY_PAIR;
//此处只有在授权服务器和资源服务器在一起的时候才能这样搞,实际使用RSA还是需要用JDK和openssl去生成证书
static {
try {
KEY_PAIR = KeyPairGenerator.getInstance("RSA").generateKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Autowired
private SystemAccountDao systemAccountDao;
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setKeyPair(KEY_PAIR);
return accessTokenConverter;
}
@Bean
public ResourceServerTokenServices resourceJwtTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
// 使用自定义的Token转换器
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 使用自定义的tokenStore
defaultTokenServices.setTokenStore(jwtTokenStore());
return defaultTokenServices;
}
/**
* token信息扩展
*/
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new TokenEnhancer() {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
String userName = userAuthentication.getName();
List<SystemAccount> list = systemAccountDao.findByAccount(userName);
if (list != null && !list.isEmpty()) {
SystemAccount account = list.get(0);
Map<String, Object> additionalInformation = new HashMap<>();
Map<String, String> map = new HashMap<>();
map.put("account", account.getAccount());
map.put("createTime", account.getCreateTime().toString());
map.put("state", String.valueOf(account.getState()));
additionalInformation.put("user", GsonUtil.toJson(map));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
}
}
return accessToken;
}
};
}
}
配置UserService
@Component
public class AuthUserDetailsService implements UserDetailsService {
@Autowired
private SystemAccountDao systemAccountDao;
@Autowired
private SystemAccountRoleDao systemAccountRoleDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<SystemAccount> list = systemAccountDao.findByAccount(username);
if (list != null && !list.isEmpty()) {
SystemAccount account = list.get(0);
List<SystemAccountRole> roleList = systemAccountRoleDao.findByAccountId(account.getId());
Collection<SimpleGrantedAuthority> authorities = new HashSet<>();
if (roleList != null && !roleList.isEmpty()) {
for (SystemAccountRole accountRole : roleList) {
authorities.add(new SimpleGrantedAuthority(String.valueOf(accountRole.getRoleId())));
}
}
return new User(username, account.getPassword(),
account.getState() == 1, true, true, true,
authorities);
}
return null;
}
}
password模式访问示例
password模式访问客户端模式访问示例
客户端模式访问携带token访问受保护资源
携带token访问受保护资源项目地址
示例项目代码可到我的Github上下载:https://github.com/DexterQY/authentication
网友评论