上一篇:【SpringBoot攻略十二、spring security集成】
1、pom.xml添加依赖
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
2、TestEndpoints测试用的端点即api接口
package com.javasgj.springboot.oauth.password.server;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试用的端点即api接口
*/
@RestController
public class TestEndpoints {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "order id : " + id;
}
}
3、@EnableAuthorizationServer授权服务器
package com.javasgj.springboot.oauth.password.server;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.store.redis.RedisTokenStore;
/**
* @EnableAuthorizationServer
* 配置授权服务器
*
*
* 如果要是实现JWT令牌,可以参考如下(加上网上博客):
* JWT令牌(JWT Tokens):
* 使用JWT令牌你需要在授权服务中使用 JwtTokenStore,资源服务器也需要一个解码的Token令牌的类 JwtAccessTokenConverter,
* JwtTokenStore依赖这个类来进行编码以及解码,因此你的授权服务以及资源服务都需要使用这个转换类。Token令牌默认是有签名的,并且资源服务需要验证这个签名,
* 因此呢,你需要使用一个对称的Key值,用来参与签名计算,这个Key值存在于授权服务以及资源服务之中。或者你可以使用非对称加密算法来对Token进行签名,
* Public Key公布在/oauth/token_key这个URL连接中,默认的访问安全规则是"denyAll()",即在默认的情况下它是关闭的,你可以注入一个标准的 SpEL表达式到 AuthorizationServerSecurityConfigurer这个配置中来将它开启(例如使用"permitAll()"来开启可能比较合适,因为它是一个公共密钥)。
* 如果你要使用 JwtTokenStore,请务必把"spring-security-jwt"这个依赖加入到你的classpath中。
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private DataSource dataSource;
@Autowired
private JdbcDaoImpl jdbcDaoImpl;
/**
* 配置令牌端点(Token Endpoint)的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 配置403无权限的handler,默认OAuth2AccessDeniedHandler返回一个xml错误提示
//.accessDeniedHandler(accessDeniedHandler)
//配置client_secret加密方式
//.passwordEncoder(passwordEncoder)
// /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话,默认是denyAll()
.tokenKeyAccess("permitAll()")
// /oauth/check_token:用于资源服务访问的令牌解析验证端点,默认是denyAll()
.checkTokenAccess("isAuthenticated()")
/*
* 认证客户端信息,校验client_id和client_secret
* allowFormAuthenticationForClients是为了注册clientCredentialsTokenEndpointFilter
* clientCredentialsTokenEndpointFilter,解析request中的client_id和client_secret
* 构造成UsernamePasswordAuthenticationToken,然后使用容器中的顶级身份管理器AuthenticationManager去进行身份认证
* (AuthenticationManager的实现类一般是ProviderManager。而ProviderManager内部维护了一个List,真正的身份认证是由一系列AuthenticationProvider去完成
* 而AuthenticationProvider的常用实现类则是DaoAuthenticationProvider,DaoAuthenticationProvider内部又聚合了一个UserDetailsService接口,UserDetailsService才是获取用户详细信息的最终接口
* 通过UserDetailsService实现类ClientDetailsUserDetailsService查询作简单的认证而已,这个设计是将client客户端的信息(client_id,client_secret)适配成用户的信息(username,password)
* 一般是针对password模式和client_credentials模式
*
* 当然也可以使用http basic认证,如果使用了http basic认证,就不要使用clientCredentialsTokenEndpointFilter
* 使用BasicAuthenticationFilter过滤器,解析request header中的Authorization: Basic client_id:client_secret的Base64编码
* 注意:http basic认证需要在请求头添加Authorization: Basic client_id:client_secret的Base64编码
*/
.allowFormAuthenticationForClients();
}
/**
* 配置客户端的相关信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// InMemoryClientDetailsService和JdbcClientDetailsService提供数据源给ClientDetailsUserDetailsService获取信息认证用
// 基于内存配置客户端信息,实现类InMemoryClientDetailsService,
/*clients.inMemory()
.withClient("client_1") // clientId:用来标识客户的Id
//.secret("{noop}123456") // secret:客户端安全码
.secret(passwordEncoder.encode("123456"))
.resourceIds("order") // resourceIds:客户端对哪些资源服务可以访问,如果没设置,就是对所有的resource都有访问权限
.authorizedGrantTypes("password", "refresh_token") // authorizedGrantTypes:客户端可以使用的授权类型,默认为空
.scopes("all") // scope:限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围
.authorities("client") // authorities:此客户端可以使用的权限(基于Spring Security authorities)
.and()
.withClient("client_2")
//.secret("{noop}123456")
.secret(passwordEncoder.encode("123456"))
.resourceIds("order")
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("all")
.authorities("client");*/
// 基于数据库配置客户端信息,实现类JdbcClientDetailsService
clients.jdbc(dataSource);
}
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/**
* 配置授权端点的URL(Endpoint URLs)
* defaultPath:这个端点URL的默认链接
* customPath:你要进行替代的URL链接
* 以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数
*
* /oauth/authorize: 授权端点,对应AuthorizationEndpoint
* /oauth/token: 令牌端点(获取令牌、更新令牌),对应TokenEndpoint
* /oauth/confirm_access: 用户确认授权提交端点,对应WhitelabelApprovalEndpoint
* /oauth/error: 授权服务错误信息端点,对应WhitelabelErrorEndpoint
* /oauth/check_token: 用于资源服务访问的令牌解析端点,对应CheckTokenEndpoint
* /oauth/token_key: 提供公有密匙的端点,如果你使用JWT令牌的话,,对应TokenKeyEndpoint
*
* 这里使用默认值
*/
//endpoints.pathMapping(defaultPath, customPath)
/**
* TokenEndpoint源码可知,它会调用接口TokenGranter构建令牌OAuth2AccessToken
* AuthorizationServerEndpointsConfigurer的tokenGranter()方法可知调用CompositeTokenGranter,获取相对应的TokenGranter(共五种)
*
* ResourceOwnerPasswordTokenGranter password密码模式
* AuthorizationCodeTokenGranter authorization_code授权码模式
* ClientCredentialsTokenGranter client_credentials客户端模式
* ImplicitTokenGranter implicit简化模式
* RefreshTokenGranter refresh_token 刷新token
* 根据grant_type判断调用哪个TokenGranter
*
* AbstractTokenGranter提供grant()方法生成令牌OAuth2AccessToken
* 上面五种TokenGranter都是继承AbstractTokenGranter并且覆盖相应的方法已达到各自的要求,如:
* password密码模式:ResourceOwnerPasswordTokenGranter
* 覆盖getOAuth2Authentication方法,认证用户名和密码,返回OAuth2Authentication(实现Authentication存储用户信息)
*
*
*
*/
endpoints
// password模式需要authenticationManager
.authenticationManager(authenticationManager)
// 必须指定,否则refresh_token会报错
.userDetailsService(jdbcDaoImpl)
/**
* TokenServices真正生成OAuth2AccessToken,默认DefaultTokenServices,
* 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,
* 除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore这个接口有一个默认的实现,
* 它就是 InMemoryTokenStore,所有的令牌是被保存在了内存中
*/
//.tokenServices(tokenServices)
/**
* 存储token
* InMemoryTokenStore 基于内存,默认值
* JdbcTokenStore 基于数据库
* JwtTokenStore 基于Jwt
* RedisTokenStore 基于redis,可以利用redis自身key的过期时间
*/
.tokenStore(new RedisTokenStore(redisConnectionFactory));
}
}
4、@EnableResourceServer资源服务器
package com.javasgj.springboot.oauth.password.server;
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;
/**
* @EnableResourceServer
* 配置资源服务器
*
* 主要过滤器:OAuth2AuthenticationProcessingFilter
* 使用AuthenticationManager的实现类OAuth2AuthenticationManager认证token,使用Authentication的实现类OAuth2Authentication来填充Spring Security上下文。
*
* TokenExtractor:只有一个实现类BearerTokenExtractor
* 它的作用在于分离出请求中包含的token。也启示了我们可以使用多种方式携带token
* request Header中:Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
* url拼接:http://localhost:8080/order/1?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
* form表单(post):access_token=f732723d-af7f-41bb-bd06-2636ab2be135
*/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
// 指定资源服务的resourceid(推荐),配合clients的resourceIds
.resourceId("order")
// 只能基于token认证
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/order/**").hasRole("USER") // 用户角色,.authenticated()只需认证无需授权都可以访问
.antMatchers("/product/**").access("#oauth2.hasScope('all')") // 指定资源访问范围,配合clients的scopes
//.antMatchers("/product/**").hasRole("USER")
//.antMatchers("/product/**").hasAnyRole("USER", "ADMIN")
//.antMatchers("/product/**").access("hasRole('USER') or hasRole('ADMIN')")
.anyRequest().permitAll()
.and()
.csrf().disable();
}
}
5、@EnableWebSecurity spring security配置
package com.javasgj.springboot.oauth.password.server;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 测试,用户保存在内存中
/*auth.inMemoryAuthentication()
.withUser("user_1").password("{noop}123456").roles("USER")
.and()
.withUser("user_2").password("{noop}123456").roles("USER");*/
// 数据库加载用户
auth
//.eraseCredentials(false) // 是否清空凭证即密码
.userDetailsService(jdbcDaoImpl()) // 指定获取用户信息的userDetailsService
.passwordEncoder(passwordEncoder()); // 设置加密方式
}
/**
* 暴露AuthenticationManager,给OAuth2AuthorizationServerConfig的password模式使用
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Bean
public JdbcDaoImpl jdbcDaoImpl() {
JdbcDaoImpl jdbcDaoImpl = new JdbcDaoImpl();
jdbcDaoImpl.setDataSource(dataSource);
jdbcDaoImpl.setRolePrefix("ROLE_");
jdbcDaoImpl.setEnableGroups(true);
jdbcDaoImpl.setUsersByUsernameQuery("select username, password, enabled from t_system_user where username = ?");
jdbcDaoImpl.setAuthoritiesByUsernameQuery("select username, privilege_code from t_system_user_privilege where username = ?");
jdbcDaoImpl.setGroupAuthoritiesByUsernameQuery("select a.id, a.name, c.privilege_code from t_system_role a, t_system_user_role b, t_system_role_privilege c where a.id = b.role_id and b.role_id = c.role_id and b.username = ?");
return jdbcDaoImpl;
}
/**
* 一旦暴露PasswordEncoder,全局默认的无加密方式就会失效,所以授权服务clients的密码也需要用此方式加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6、启动类Application
package com.javasgj.springboot.oauth.password.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* spring boot应用启动类
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
7、测试postman
访问:http://127.0.0.1:8080/springboot/order/1
提示:
接着,我们使用postman
这样我们就获取到token,那么访问资源时带上token应该就能成功了:
更新令牌
注意:
scope说明
当客户配置scope=all,select
获取token时没有指定scope,那么这个token拥有所有权限即all和select,那么下面资源可以访问
因为product/**需要all的权限;
如果获取token时指定scope=select,那么这个token只拥有select权限而没有all的权限,product/**需要all的权限就不能访问了,返回如下错误:
令牌更新会继承第一次获取token时的scope!
很有用的两张图