前言
oauth2规范中具备了四种授权模式,分别如下:
·授权码模式:authorization code
·简化模式:implicit
·密码模式:resource owner password credentials
·客户端模式:client credentials
注:本示例只演示密码模式,感兴趣的同学自己花时间测试另外三种授权模式。
配置mongodb和jwt
1、新建Application入口应用类
@SpringBootApplication
@RestController
@EnableEurekaClient
// 该服务将作为OAuth2服务
@EnableAuthorizationServer
// 注意:不加@EnableResourceServer注解,下面user信息为空
@EnableResourceServer
public class Application {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@GetMapping("/user")
public Map<String, Object> user(OAuth2Authentication user){
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、新建JWTOAuth2Config类
@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsService mongoClientDetailsService;
@Autowired
private UserDetailsService mongoUserDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private DefaultTokenServices tokenServices;
// 将JWTTokenStore类中的JwtAccessTokenConverter关联到OAUTH2
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 自动将JWTTokenEnhancer装配到TokenEnhancer类中
// token增强类,需要添加额外信息内容的就用这个类
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// /oauth/token
// 如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走
// ClientCredentialsTokenEndpointFilter来保护
// 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// Spring Oauth 允许开发人员挂载多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain类中
// 设置jwt签名和jwt增强器到TokenEnhancerChain
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.tokenStore(tokenStore)
// 在jwt和oauth2服务器之间充当翻译(签名)
.accessTokenConverter(jwtAccessTokenConverter)
// 令牌增强器类:扩展jwt token
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(mongoUserDetailsService);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用mongodb保存客户端信息
clients.withClientDetails(mongoClientDetailsService);
}
}
3、新建JWTTokenEnhancer令牌增强器类
// 令牌增强器类
public class JWTTokenEnhancer implements TokenEnhancer {
// 要进行增强需要覆盖enhance方法
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
String newContent ="这是新加的内容";
additionalInfo.put("newContent", newContent);
// 所有附加的属性都放到HashMap中,并设置在传入该方法的accessToken变量上
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
return oAuth2AccessToken;
}
}
4、新建JWTTokenStoreConfig类以支持jwt
// 用于定义Spring将如何管理JWT令牌的创建、签名和翻译
@Configuration
public class JWTTokenStoreConfig {
@Autowired
private ServiceConfig serviceConfig;
// 设置TokenStore为JwtTokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
// @Primary作用:如果有多个特定类型bean那么就使用被@Primary标注的bean类型进行自动注入
@Bean
@Primary
public DefaultTokenServices tokenServices() {
// 用于从出示给服务的令牌中读取数据
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
// 在jwt和oauth2服务器之间充当翻译
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 定义将用于签署令牌的签名密钥(自定义 存储在git上authentication.yml文件)
// jwt是不保密的,所以要另外加签名验证jwt token
converter.setSigningKey(serviceConfig.getJwtSigningKey());
return converter;
}
// 设置TokenEnhancer增强器中使用JWTTokenEnhancer增强器
@Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}
}
5、新建WebSecurityConfigurer类,设置访问权限、跨域CORS以及基本配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
BCryptPasswordEncoder passwordEncoder;
// 用来处理用户验证
// 被注入OAuth2Config类中的 endpoints方法中
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
@Autowired
private UserDetailsService userDetailsService;
// 定义用户、密码和用色的地方
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
//不加这段代码不显示返回的json信息
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 设置成form登录,前端就要使用form-data传参
// .formLogin()
// 设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
.cors()
.and()
.httpBasic()
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 不拦截Options请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 注意:表示所有的访问都必须进行用户认证处理后才可以访问
// .antMatchers("/register/**").permitAll()
.anyRequest().authenticated().and()
.csrf().disable()
.headers()
// 禁用缓存
.cacheControl();
}
@Override
public void configure(WebSecurity web) {
// 注意:全局忽略访问限制
web.ignoring().antMatchers("/register/**");
}
// 解决跨域CORS问题
@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}
6、新建MongoClientDetailsService类,校验及更新mongodb存储的客户端信息
@Service("mongoClientDetailsService")
public class MongoClientDetailsService implements ClientDetailsService {
private final String CONLLECTION_NAME = "oauth_client_details";
@Autowired
MongoTemplate mongoTemplate;
@Autowired
BCryptPasswordEncoder passwordEncoder;
// private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
BaseClientDetails client = mongoTemplate.findOne(new Query(Criteria.where("clientId").is(clientId)), BaseClientDetails.class, CONLLECTION_NAME);
if(client==null){
throw new RuntimeException("没有查询到客户端信息");
}
return client;
}
public void addClientDetails(ClientDetails clientDetails) {
mongoTemplate.insert(clientDetails, CONLLECTION_NAME);
}
public void updateClientDetails(ClientDetails clientDetails) {
Update update = new Update();
update.set("resourceIds", clientDetails.getResourceIds());
update.set("clientSecret", clientDetails.getClientSecret());
update.set("authorizedGrantTypes", clientDetails.getAuthorizedGrantTypes());
update.set("registeredRedirectUris", clientDetails.getRegisteredRedirectUri());
update.set("authorities", clientDetails.getAuthorities());
update.set("accessTokenValiditySeconds", clientDetails.getAccessTokenValiditySeconds());
update.set("refreshTokenValiditySeconds", clientDetails.getRefreshTokenValiditySeconds());
update.set("additionalInformation", clientDetails.getAdditionalInformation());
update.set("scope", clientDetails.getScope());
mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientDetails.getClientId())), update, CONLLECTION_NAME);
}
public void updateClientSecret(String clientId, String secret) {
Update update = new Update();
update.set("clientSecret", secret);
mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientId)), update, CONLLECTION_NAME);
}
public void removeClientDetails(String clientId) {
mongoTemplate.remove(new Query(Criteria.where("clientId").is(clientId)), CONLLECTION_NAME);
}
public List<ClientDetails> listClientDetails(){
return mongoTemplate.findAll(ClientDetails.class, CONLLECTION_NAME);
}
}
7、新建MongoUserDetailsService类,检验存储的用户数据
@Service
public class MongoUserDetailsService implements UserDetailsService {
private final String USER_CONLLECTION = "userAuth";
@Autowired
MongoTemplate mongoTemplate;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// identifier:1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博
UserAuth userAuth = mongoTemplate.findOne(new Query(Criteria.where("identifier").is(username)), UserAuth.class, USER_CONLLECTION);
if(userAuth == null) {
throw new RuntimeException("没有查询到用户信息");
}
return new User(username, userAuth.getCertificate(), mapToGrantedAuthorities(userAuth.getRoles()));
}
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
8、实体类
User.java
@Setter
@Getter
public class User {
private String id;
private String uid;
//用户名
private String userName;
//用户昵称
private String nickName;
//是否是超级管理员
private boolean admin;
// 性别
private String gender;
// 生日
private Long birthday;
//个性签名
private String signature;
//email
private String email;
//email
private Long emailBindTime;
//mobile
private String mobile;
//mobile
private Long mobileBindTime;
// 头像
private String face;
// 头像200*200
private String face200;
// 原图图像
private String srcface;
//状态 2正常用户 3禁言用户 4虚拟用户 5运营
private Integer status;
// 类型
private Integer type;
}
UserAuth.java
@Getter
@Setter
public class UserAuth {
// id
private String id;
private String uid;
// 1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博
private Integer identityType;
// 手机号 邮箱 用户名或第三方应用的唯一标识
private String identifier;
// 密码凭证(站内的保存密码,站外的不保存或保存token)
private String certificate;
// md5 盐值加密
private String md5;
//角色ID
private List<String> roles;
}
9、表结构
oauth_client_details:
{
"_id" : ObjectId("5f01e1cf1315d14f5bea1679"),
"clientId" : "core-resource",
"resourceIds" : "card",
"clientSecret" : "$2a$10$8NUXEVgWW72Gf.QQtQlsQu1L9KGxAonW.QfO3s82Kr9DADL4wn24K",
"authorizedGrantTypes" : "password,authorization_code,refresh_token",
"registeredRedirectUris" : "http://localhost:9001/base/login",
"authorities" : "",
"accessTokenValiditySeconds" : "7200",
"refreshTokenValiditySeconds" : "0",
"autoapprove" : true,
"additionalInformation" : null,
"scope" : "all"
}
user:
{
"_id" : ObjectId("5e7d56c9b03e9a046ab26cac"),
"uid" : "5e7d56c9b03e9a046ab26ca9",
"username" : "zhangwei",
"admin" : false,
"email" : "zhangwei900808@126.com",
"emailBindTime" : NumberLong(1585272521646),
"status" : 2,
"type" : 1
}
userAuth:
{
"_id" : ObjectId("5e7d56c9b03e9a046ab26caa"),
"uid" : "5e7d56c9b03e9a046ab26ca9",
"identityType" : 2,
"identifier" : "zhangwei900808@126.com",
"certificate" : "$2a$10$OdHuIooHSv60jC7YYahQB.dsfAWFA..Jdb0KwRcn9F9yrz64HPFfC",
"roles" : [
"ROLE_USER"
]
}
{
"_id" : ObjectId("5e7d56c9b03e9a046ab26cab"),
"uid" : "5e7d56c9b03e9a046ab26ca9",
"identityType" : 3,
"identifier" : "zhangwei",
"certificate" : "$2a$10$nHqwjbwAjgHeTu3.FDSDWEWe6fa/7zcFZ6bVSrrkGkEZ7OIYOdkMe",
"roles" : [
"ROLE_USER"
]
}
演示
总结:
1、oauth2保存客户端信息有好多种:内存,jdbc。像我这里使用的是mongodb
2、数据库表User保存的是用户基本信息,真正密码和访问类型(用户名、邮箱、手机号等等)是在表UserAuth里面,这个大家注意下
3、在WebSecurityConfigurerAdapter类中,设置成form登录,前端就要使用form-data传参,设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
4、AuthorizationServerConfigurerAdapter中要设置获取token的路由/oauth/token能被访问到,还要设置成下面这段代码:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
5、MongoUserDetailsService最后返回的是org.springframework.security.core.userdetails.UserDetails类,所以我们从mongodb返回自己的UserAuth之后再转换成上面的类即可
6、WebSecurityConfigurerAdapter下的configure(HttpSecurity http)方法和configure(WebSecurity web)忽略网址本质上不同,
configure(HttpSecurity http)
方法是忽略已经校验过的用户,限制它的某些权限比如:POST、GET或者只有USER或者ADMIN用户才能访问
configure(WebSecurity web)
则是全局忽略,比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor、HttpSecurity
具体的权限控制规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 设置成form登录,前端就要使用form-data传参
// .formLogin()
// 设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
.httpBasic()
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 注意:表示所有的访问都必须进行用户认证处理后才可以访问
// .antMatchers("/register/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable().cors();
}
@Override
public void configure(WebSecurity web) {
// 注意:全局忽略访问限制
web.ignoring().antMatchers("/register/**");
}
引用
Spring Security Oauth2 授权服务开发之MongoDB
解决Spring Security OAuth在访问/oauth/token时候报401 authentication is required
Spring cloud oauth2 研究--oauth\_client\_detail表说明
关于spring boot security设置忽略地址不生效问题
------------------ 2021-5-18 更新-------------------
注意:当我们使用password模式时,client_id和client_secret可以不通过request body传值,而是通过basic auth传值,这样会在请求header中添加一个:Authorization:basic base64位的加密值,传递过去,跟request body请求效果相同,如下所示:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。