参照上次 Spring Security + JWT 的简单应用
一、建立一个Springboot项目,最终的项目结构如下
二、添加pom依赖
org.springframework.security.oauth
spring-security-oauth2
2.0.12.RELEASE
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.9.RELEASE
org.projectlombok
lombok
true
三、习惯性修改yml
server:
port: 6008
四、建立好一个预设的文件
1. 常量表
package com.chris.oauth.consts;
/**
* create by: Chris Chan
* create on: 2019/9/26 8:08
* use for: 全局常量
*/
public interface AppConstants {
//JWT默认指纹
String JWT_SECRET_KEY_NORMAL="JKHDWNCJKLFKKJHDKEKLLDKJKLFHJKHSGHAJKFJLNKSFLLKDLKL";
//响应头Authorization
String KEY_AUTHORIZATION="Authorization";
}
2. 用户信息实体
package com.chris.oauth.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* create by: Chris Chan
* create on: 2019/9/26 8:09
* use for:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private String username;
private String password;
}
五、编写与Security相关的几个文件
1. SecurityConfig配置
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
/**
* create by: Chris Chan
* create on: 2019/10/12 2:13
* use for:
*/
@Configuration
@EnableWebSecurity
@EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userService)
.passwordEncoder(passwordEncoder);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
在这个配置文件中放行"/oauth/token"接口URL,这是我们oauth需要用到的主要接口。这个配置文件会在oauth2的配置文件之前执行。
2. UserService
package com.chris.oauth.service;
import com.chris.oauth.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* create by: Chris Chan
* create on: 2019/10/11 12:31
* use for:
*/
@Service
public class UserService implements UserDetailsService {
private static Map userMap = new HashMap<>(16);//用户
private static Map userAuthMap = new HashMap<>(16);//用户权限
private static Map userAdditionalMap = new HashMap<>(16);//用户附加信息 测试
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
return null;
}
UserInfo userInfo = findUser(username);
if (null == userInfo) {
return null;
}
return new User(username, userInfo.getPassword(), getAuthorityList(username));
}
/**
* 返回密码
* 这个方法可以假设是从数据库dao层获取到用户信息
*
* @param username
* @return
*/
private UserInfo findUser(String username) {
if (null == userMap) {
userMap = new HashMap<>(16);
}
//内置几个用户
if (userMap.size() == 0) {
userMap.put("zhangsanfeng", passwordEncoder.encode("123123"));
userMap.put("lisifu", passwordEncoder.encode("123123"));
userMap.put("songzihao", passwordEncoder.encode("123123"));
}
String password = userMap.get(username);
if (StringUtils.isEmpty(password)) {
return null;
}
return new UserInfo(username, password);
}
/**
* 获取用户权限
* 这个方法也可以在数据库中查询
*
* @param username
* @return
*/
public static String[] getAuthorities(String username) {
if (null == userAuthMap) {
userAuthMap = new HashMap<>(16);
}
//内置几个用户权限
if (userAuthMap.size() == 0) {
userAuthMap.put("zhangsanfeng", "ROLE_ADMIN,ROLE_USER");
userAuthMap.put("lisifu", "ROLE_ADMIN,ROLE_USER");
userAuthMap.put("songzihao", "ROLE_SYS,ROLE_ADMIN,ROLE_USER");
}
return userAuthMap.get(username).split(",");
}
/**
* 获取用户权限集合
*
* @param username
* @return
*/
public static List getAuthorityList(String username) {
return AuthorityUtils.createAuthorityList(getAuthorities(username));
}
/**
* 获取用户的附加信息 测试使用 用以扩展JWT
*
* @param username
* @return
*/
public static String getUserAdditional(String username) {
if (null == userAdditionalMap) {
userAdditionalMap = new HashMap<>(16);
}
//内置几个用户权限
if (userAdditionalMap.size() == 0) {
userAdditionalMap.put("zhangsanfeng", "worker");
userAdditionalMap.put("lisifu", "student");
userAdditionalMap.put("songzihao", "teacher");
}
return userAdditionalMap.get(username);
}
}
增加了一个用户附加信息,这些在实际业务中都是可以从数据库中去查询的,测试目的是扩展jwt的信息。
六、有关的Bean的管理,由于Bean比较多,所以我们把他们收集起来管理。
package com.chris.oauth.config;
import com.chris.oauth.consts.AppConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* create by: Chris Chan
* create on: 2019/10/12 12:35
* use for:
*/
@Configuration
public class BeanBox {
/**
* 密码编码器
* 这个实例中security和oauth都是用一个密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* jwt生成处理
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* token转换器
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
// converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
converter.setSigningKey(AppConstants.JWT_SECRET_KEY_NORMAL);//设置JWT指纹
return converter;
}
}
七、JWT扩展器
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* create by: Chris Chan
* create on: 2019/10/12 12:39
* use for: JWT扩展处理
*/
@Component
@Primary
public class JWTTokenEnhancer implements TokenEnhancer {
@Autowired
UserService userService;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
//添加附加信息
Map map = new HashMap<>(16);
map.put("additional", userService.getUserAdditional(username));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
//设置过期时间
((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(LocalDateTime.now().plusDays(1).toInstant(ZoneOffset.of("+8")).toEpochMilli()));
return accessToken;
}
}
默认的JwtTokenStore产生的jwt只包含用户名和权限列表,如果我们需要为登录用户在jwt中添加其他的必要信息,就要编写这个。
八、与OAuth2相关的两个配置文件
package com.chris.oauth.config;
import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.ArrayList;
import java.util.List;
/**
* create by: Chris Chan
* create on: 2019/10/12 2:15
* use for:
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserService userService;
@Autowired
TokenStore tokenStore;
@Autowired
TokenEnhancer tokenEnhancer;
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private AuthenticationManager authenticationManager;
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("clientId")
.secret(passwordEncoder.encode("clientSecret"))
.redirectUris("http://localhost:6008/callback")
.authorizedGrantTypes("authorization_code", "password", "implicit", "client_credentials", "refresh_token")
.resourceIds("resId")
.scopes("scope1", "scope2");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
.userDetailsService(userService)
.authenticationManager(authenticationManager);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List enhancerList = new ArrayList<>();
enhancerList.add(tokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints.tokenEnhancer(enhancerChain)
.accessTokenConverter(jwtAccessTokenConverter);
}
}
租后一个方法就是配置jwt和扩展工具等等。
package com.chris.oauth.config;
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;
/**
* create by: Chris Chan
* create on: 2019/9/27 16:51
* use for:
*/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
}
九、测试
程序运行起来,我们用PostMan来测试
1. 把所有参数全部添加到url中:
localhost:6008/oauth/token?grant_type=password&username=songzihao&password=123123&client_id=clientId&client_secret=clientSecret
因为/oauth/token是post请求方式,不可以直接在浏览器地址栏请求。
结果:
2. client_id和client_secret不出现在url中,而是用basic auth的方式提交
结果相同。
3. 把client_id和client_secret用英文冒号连接,进行base64url编码,然后放到头部信息Authorization中,加Basic 前缀。
得到正确的结果。
4. 我们再把访问token中有效载荷部分解析一下,会看到我们放置在其中的用户名、权限列表、附加信息,client_id,还有我们自己设置的过期时间
有效时间被我设置为一天。
本次测试圆满完成。
九、填坑记
1. 本次主要目的是测试password模式,不过在加上配置文件之后,password模式必须要AuthenticationManager的配置;
2. 在password模式下,总是出现很多次调用太深的error,我把yml中配置的用户名和密码去掉,在内存中放置的用户信息,并且指定了密码编码器才得以解决。
十、说明
在分布式中,oauth2只是用来提供jwt,分布式业务服务中,是接收访问令牌并加以解析,同样的,需要提供相同的JWT资料,比如指纹,或者秘钥文件。