源码:TokenEndpoint
在org.springframework.security.oauth2.provider.endpoint包下
1.获取登录对象信息
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
2.从返回的token信息获取ip地址(additionalInformation里面是自定义的信息,ipAddress也是自定义的)
String ipAddress = accessToken.
getAdditionalInformation().get("ipAddress").toString();
3.退出登录
//自己封装 实际调用tokenStore.removeAccessToken(existingAccessToken);
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenId = authorization.substring("Bearer".length() + 1);
tokenServices.revokeToken(tokenId);
}
//源码里面 existingAccessToken的获取见下面两个方法
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(existingAccessToken);
//根据tokenValue获取accessToken
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
//根据authentication获取accessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
4.获取request和request的Ip
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ipAddress = existingAccessToken.
getAdditionalInformation().get(AuthConstant.TOKEN_IP_ADDRESS_KEY).toString();
一。搭建微服务开放平台的目的与达到的效果
一。搭建微服务开放平台的目的与达到的效果
目的:管理微服务中的开放接口,对接口进行授权认证
达到的效果:配置一下appId和appSecret,就自动有如下功能
a.获取授权码:
http://localhost:8080/oauth/authorize? client_id=c1&response_ype=code&scope=all&redirect_uri=http://www.baidu.com
b.获取accessToken
http://localhost:8080/oauth/token? //通过授权码token
grant_type=authorization_code client_id=c1 client_secret=secret code=5PgfcD redirect_uri=http://www.baidu.com
http://localhost:8080/oauth/token? //通过密码模式获取token(密码模式拿token不要先获取授权码)
grant_type=password client_id=c1 client_secret=secret username="张三” password="123456" scope="all"
c.刷新token
http://localhost:8080/oauth/toekn?
grant_type=refresh_token refresh_token="454df" client_id client_secret
d.验证token是否有效
http://localhost:8080/oauth/check_token?
token="sdfw555sd45d4f"
二。搭建授权服务中心 (类似微信平台 星云授权项目)
1.导入pom依赖 pom依赖见githua地址:https://gitee.com/four_hundred_and_twentyone/spring-security-oauth.git
<!-- spring-cloud-starter-oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
2..两个配置类java文件 里面重写多个configure方法 配置了哪些客户秘钥/哪些资源服务可以访问本授权服务,token有效期等信息
一个AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter
一个WebSecurityConfig 继承WebSecurityConfigurerAdapter
3.主程序启动测试 获取授权码 获取accessToken等功能
三。搭建资源服务中心 (类似一个app应用 星云业务项目)
//这种方式是在微服务进行认证拦截,也可以统一在网关处拦截
1.导入pom依赖 和资源服务一致
<!-- spring-cloud-starter-oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
2.主要一个类OAuth2ServerConfig 继承 ResourceServerConfigurerAdapter 类似加注解
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(this.resourceId) //资源id
}
public void configure(HttpSecurity http){
http.authorizeRequests().antMatchers(antMather).authenticated() //拦截antMatcher请求
}
}
3.resouces下配置文件配置授权服务中心地址
security:
oauth2:
client:
client-id: web //appId
client-secret: nebulait //appSecret
resource:
token-info-uri: http://localhost:8589/oauth/check_token //从认证授权中心上验证token
4.主程序启动访问测试:http://localhost:8080/api/order/test
Headers要加 Authorization : Bearer b2011201-e7cb-4977-a418-768dd5b43a53
或者请求路径后面加上参数 http://localhost:8080/api/order/test?token=b2011201-e7cb-4977-a418-768dd5b43a53
四。代码案例
1、授权服务器配置
一个WebSecurityConfig 继承 WebSecurityConfigurerAdapter
AuthorizationServerConfig 继承 AuthorizationServerConfigurerAdapter
//WebSecurityConfig
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//拦截所有请求,使用http-basic方式登录
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
}
}
AuthorizationServerConfig
//简化版
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//1.加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//2.用户信息 正常从数据库读取
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build() );
userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build());
return userDetailsService;
}
//3.整合加密方式和用户信息
@Bean
AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
//4.啥东东
@Bean
AuthenticationManager authenticationManager() {
AuthenticationManager authenticationManager = new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return daoAuthenticationProvider().authenticate(authentication);
}
};
return authenticationManager;
}
//1.配置appId secret 回到地址 token有效期
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("client_1")
.secret(passwordEncoder().encode("secret_1"))
.redirectUris("https://www.baidu.com/")
.authorizedGrantTypes("password", "refresh_token", "authorization_code")
.scopes("all")
.resourceIds("resourceId1")
.accessTokenValiditySeconds(36000)
.refreshTokenValiditySeconds(36000)
.and()
.withClient("client_2")
.secret(passwordEncoder().encode("secret_2"))
.redirectUris("https://www.baidu.com/")
//授权码模式、密码模式、简单模式、客户端模式、regresh_token获取token
.authorizedGrantTypes("authorization_code","password","implicit","client_credentials" "refresh_token",)
.scopes("all")
.resourceIds("resourceId2")
.accessTokenValiditySeconds(36000)
.refreshTokenValiditySeconds(36000)
;
}
//2.配置token类型
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager())
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
.userDetailsService(userDetailsService())
;
}
//3.配置权限
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.allowFormAuthenticationForClients() //允许表单认证
.checkTokenAccess("permitAll()"); //验证token
}
//星云版
@Configuration
@EnableAuthorizationServer //开启授权认证服务中心 在资源中心开启资源服务中心 @EnableResourceServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override //配置客户端
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory() //暂时使用内存模式
.withClient("web") //appId
.secret(passwordEncoder().encode("nebulait")) //客户端秘钥
.resourceIds(OPERATION_RESOURCE_ID, TIMED_TASK_RESOURCE_ID) //可以访问的资源列表
.authorizedGrantTypes("password", "refresh_token","authorization_code") //允许的授权类型
.scopes("all") //授权范围(客户端权限)
.accessTokenValiditySeconds(36000); //Token 的有效期 10小时
.redirectUris("www.baidu.com") //回调地址
}
@Override //配置令牌 的访问端点和服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) //允许Post,get提交
.tokenStore(tokenStore())
.authenticationManager(authenticationManager) //autorizationCodeServive 授权码模式用这个
.userDetailsService(userDetailService) //密码模式需要
.accessTokenConverter(defaultAccessTokenConverter)
.tokenEnhancer(new CustomTokenEnhancer())
.tokenServices(tokenServices()) //令牌管理服务
.exceptionTranslator(new CustomWebResponseExceptionTranslator());
}
@Override //配置令牌的端点约束
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.checkTokenAccess("isAuthenticated()"); //允许表单认证
}
}
授权服务器配置文件中 配置客户端时有多个appId 上面的第一个配置客户端改版
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("wxmini")
.resourceIds(AUTHORIZATION_RESOURCE_ID, WXMINI_RESOURCE_ID, PAYMENT_RESOURCE_ID, ORDER_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.secret(passwordEncoder().encode("nebulait"))
.accessTokenValiditySeconds(accessTokenValiditySeconds)//一月失效
.and()
.withClient("app")
.resourceIds(AUTHORIZATION_RESOURCE_ID, APP_RESOURCE_ID, PAYMENT_RESOURCE_ID, ORDER_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.secret(passwordEncoder().encode("nebulait"))
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.and()
.withClient("drainage")
.resourceIds(AUTHORIZATION_RESOURCE_ID, ORDER_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("all")
.secret(passwordEncoder().encode("nebulait"))
.redirectUris()
.accessTokenValiditySeconds(-1);//永不失效
}
绑定数据库:clients.jdbc(dataSource);
2.资源服务器配置
一个文件ResourceServerConfig 继承 ResourceServerConfigurerAdapter
//简化版
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("resourceId1");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/api/**").authenticated();
}
}
//星云版
@Configuration
@EnableResourceServer
public class ResourceServerConfi extends ResourceServerConfigurerAdapter {
@Value("${nebula.security.resourceId}")
private String resourceId;
@Value("${nebula.security.antMather}")
private String antMather;
@Value("${nebula.security.noMather}")
private String noMather;
private final DefaultWebSecurityExpressionHandler expressionHandler;
@Inject
public OAuth2ServerConfig(DefaultWebSecurityExpressionHandler expressionHandler) {
this.expressionHandler = expressionHandler;
}
public void configure(ResourceServerSecurityConfigurer resources) {
resources
.resourceId(this.resourceId) //资源id
.stateless(true)
.authenticationEntryPoint(new AuthExceptionEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.expressionHandler(this.expressionHandler);
}
public void configure(HttpSecurity http) throws Exception {
String[] urls = null;
if (!StringUtils.isEmpty(this.noMather)) {
urls = this.noMather.split(",");
}
if (urls != null) {
((AuthorizedUrl)((AuthorizedUrl)http.authorizeRequests()
.antMatchers(urls)).permitAll() //usrls是nomathers里面的内容,都放行
.antMatchers(new String[]{this.antMather})).authenticated() //antMather里面的要认证
.accessDecisionManager(this.accessDecisionManager());
} else {
((AuthorizedUrl)http.authorizeRequests()
.antMatchers(new String[]{this.antMather})).authenticated() //拦截antMatcher请求
.accessDecisionManager(this.accessDecisionManager());
}
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList();
decisionVoters.add(new WebExpressionVoter());
decisionVoters.add(new URLBasedVoter());
return new UnanimousBased(decisionVoters);
}
}
五。完善
如上就可以完成测试获取授权码 获取令牌 刷新令牌 校验令牌 资源服务访问认证的功能,下面是做一些完善配置
资源需要哪些权限:WebSecurityConfig
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
http.csrf().disable().authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated() //所有的 /r开头请求必须认证通过
.anyRequest().permitAll(); //除了 /r开头请求 其他可以访问
用户拥有哪些权限:AuthorizationServerConfig
userDetailsService.createUser(User
.withUsername("user_1")
.password(passwordEncoder().encode("123456"))
.authorities("ROLE_USER").build()
);
完善1:独立一个userDetailsService文件
(主要是实现UserDetailsService接口重写loadUserByName方法 ) 用户还没有读取数据库
//步骤1.新建一个类
@Component
public class MyUserDetailsService implements UserDetailsService {
@Bean
private PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User.withUsername("myuser").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build();
return null;
}
}
//步骤2.把这个类注入到AuthorizationServerConfig使用,去掉之前的userDetailService
@Autowired
private MyUserDetailsService myUserDetailsService;
原先userDetailsService() 内容如下
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build() );
userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build());
return userDetailsService;
}
//步骤3:AuthorizationServerConfig配置类的第二个configure,换成新注入的myUserDetailsService
.userDetailsService(myUserDetailsService)
完善2 用户的账号密码查询数据库
步骤1:pom添加加数据库驱动依赖
步骤2:yml添加数据库连接信息 这两步参考springBoot项目搭建里面的整合mybatis
步骤3:写一个dao通过userName查询用户信息
上面都是做数据库查询处理
步骤4: 修改MyUserDetailsService文件的loadUserByUsername方法
根据用户名查询数据库,没有返回null;有返回数据库的用户名和密码
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
OrgOperationUser orgOperationUser = this.selectUserByName(userName);
if (orgOperationUser == null){
return null;
}
//UserDetails userDetails = User.withUsername("myuser").password(passwordEncoder().encode("123")).authorities("ROLE_USER").build();
UserDetails userDetails = User.withUsername(orgOperationUser.getOou_name()).password(passwordEncoder().encode(orgOperationUser.getOou_password())).authorities("ROLE_USER").build();
return userDetails;
}
本来是如下写死的
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
UserDetails userDetails = User.withUsername("myuser").password(passwordEncoder().encode("123")).authorities("ROLE_USER").build();
return userDetails;
}
完善3 权限查询数据库
权限的硬编码写在MyUserDetailsService
return User.withUsername(name).password(password().authorities("p1","p2").build();
思路:数据库查询用户权限,得到权限集合,然后转为数组array,上面.authorities("p1","p2")改为.authorities(array)
//用户权限
List<String> userpermissionList = this.selectUserPermissions(orgOperationUser.getOou_id());
//用户权限集合转数组
String[] array = new String[userpermissionList.size()];
userpermissionList.toArray(array);
return User.withUsername(orgOperationUser.getOou_name()).password(orgOperationUser.getOou_password()).authorities(array).build();
资源服务获取权限
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
资源服务获取用户信息
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
完善4 MyUserDetailsService 用户返回的信息自定义
步骤一核心点:返回的用户信息MyUserDetails 实现 UserDetails 接口
本来:
UserDetails userDetails = User.withUsername(orgOperationUser.getOou_name()).password(orgOperationUser.getOou_password()).authorities(array).build();
return userDetails;
改造后:
MyUserDetails myUserDetails = this.buildMyUserDetails(orgOperationUser);
return myUserDetails;
//MyUserDetails 实现 UserDetails 接口
private MyUserDetails buildMyUserDetails(OrgOperationUser orgOperationUser) {
MyUserDetails myUserDetails = new MyUserDetails();
myUserDetails.setId(orgOperationUser.getOou_id());
myUserDetails.setCarrierId(orgOperationUser.getOou_carrier_id());
myUserDetails.setDeptId(orgOperationUser.getOou_dept_id());
myUserDetails.setCode(orgOperationUser.getOou_code());
myUserDetails.setUsername(orgOperationUser.getOou_name());
myUserDetails.setPassword(orgOperationUser.getOou_password());
return myUserDetails;
}
如上都在MyUserDetailsService 文件里
步骤二:写一个类CustomUserAuthenticationConverter继承DefaultUserAuthenticationConverter
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<>();
MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
response.put(USERNAME, userDetails);
if (!CollectionUtils.isEmpty(authentication.getAuthorities())) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
步骤三:在配置文件AuthorizationServerConfig的第二个configure加上如下两个代码
//2.配置token类型
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
完善5 SecurityContextHolder.getContext().getAuthentication().getAuthorities()有用户权限
1.数据库查询权限(这边先角色) 角色集合遍历封装成 List<GrantedAuthority>类型
private List<GrantedAuthority> selectUserPermissions(String userId) {
List<GrantedAuthority> authorities = new ArrayList<>();
//用户角色
List<String> roleList = orgOperationUserMapper.userPermissions(userId);
for (String role:roleList){
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role);
authorities.add(simpleGrantedAuthority);
}
return authorities;
}
2.buildMyUserDetails 设置这个集合
private MyUserDetails buildMyUserDetails(OrgOperationUser orgOperationUser) {
MyUserDetails myUserDetails = new MyUserDetails();
myUserDetails.setId(orgOperationUser.getOou_id());
myUserDetails.setCarrierId(orgOperationUser.getOou_carrier_id());
myUserDetails.setDeptId(orgOperationUser.getOou_dept_id());
myUserDetails.setCode(orgOperationUser.getOou_code());
myUserDetails.setUsername(orgOperationUser.getOou_name());
myUserDetails.setPassword(orgOperationUser.getOou_password());
if (!CollectionUtils.isEmpty(selectUserPermissions(orgOperationUser.getOou_id()))){
myUserDetails.setAuthorities(selectUserPermissions(orgOperationUser.getOou_id()));
}
return myUserDetails;
}
完善6 web授权(即请求路径)
在WebSecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/test1").hasAuthority("运维总监")
.antMatchers("/api/test2").hasAuthority("p2")
.antMatchers("/api/**").fullyAuthenticated() //所有的 api开头请求必须认证通过
.anyRequest().permitAll();
}
完善7:返回的token信息 添加ip地址
1.写一个类继承TokenEnhancer并重写TokenEnhancer方法
public class MyTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
HttpServletRequest request = ((ServletRequestAttributes) Objects
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
additionalInfo.put("ipAddress", request.getRemoteHost());
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
return oAuth2AccessToken;
}
}
2.配置类
//自定义token返回信息
@Bean
TokenEnhancer tokenEnhancer(){
return new MyTokenEnhancer();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
endpoints
.authenticationManager(authenticationManager())
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
.userDetailsService(myUserDetailsService)
.accessTokenConverter(defaultAccessTokenConverter)
.tokenEnhancer(tokenEnhancer()) //自定义token返回信息
;
}
完善8:客户端信息clientId,secretId从写死改成数据库读取
授权服务器的 AuthorizationServerConfig
本来写死如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("client_1")
.secret(passwordEncoder().encode("secret_1"))
.redirectUris("https://www.baidu.com/")
.authorizedGrantTypes("password", "refresh_token", "authorization_code")
.scopes("all")
.resourceIds(USER_SERVER_RESOURCE_ID)
.accessTokenValiditySeconds(36000)
.refreshTokenValiditySeconds(36000)
.and()
.withClient("client_2")
.secret(passwordEncoder().encode("secret_2"))
.redirectUris("https://www.baidu.com/")
.authorizedGrantTypes("password", "refresh_token", "authorization_code")
.scopes("all")
.resourceIds("resourceId2")
.accessTokenValiditySeconds(36000)
.refreshTokenValiditySeconds(36000)
;
}
从数据读取
1。建如下表:表名表字段都固定
CREATE TABLE "public"."oauth_client_details" (
"client_id" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"resource_ids" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"client_secret" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"SCOPE" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"authorized_grant_types" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"web_server_redirect_uri" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"authorities" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"access_token_validity" int4 DEFAULT NULL,
"refresh_token_validity" int4 DEFAULT NULL,
"additional_information" text COLLATE "pg_catalog"."default" DEFAULT NULL,
"autoapprove" text COLLATE "pg_catalog"."default" DEFAULT NULL
);
2.依赖注入数据源
@Autowired
private DataSource dataSource;
3、客户端信息
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
4.配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
完善9:token存储位置策略 内存 jwt 或者redis或者JDBC
方案1:jwt
1.添加依赖
<!‐‐spring secuity对jwt的支持,spring cloud oauth2已经依赖,可以不配置‐‐>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring‐security‐jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
2.添加JwtTokenStoreConfig文件
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private DataSource dataSource;
//redis存储
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
//数据库存储
@Bean
public TokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
//jwt存储
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //配置JWT使用的秘钥
accessTokenConverter.setSigningKey("123123");
return accessTokenConverter;
}
}
3.授权服务配置文件
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
endpoints
.tokenStore(tokenStore) //指定token存储策略是jwt
.accessTokenConverter(jwtAccessTokenConverter) //token解析器
.authenticationManager(authenticationManager())
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
.userDetailsService(myUserDetailsService)
.accessTokenConverter(defaultAccessTokenConverter)
.tokenEnhancer(tokenEnhancer())
;
}
方案2:token存储在redis
1.pom加redis依赖
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.yml文件配置redis
#redis
spring:
redis:
host: 192.168.61.143 # Redis服务器地址
port: 6379 # Redis服务器连接端口
password:
timeout: 5000 #连接超时时间(毫秒)
database: 3
3.TokenStoreConfig
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private DataSource dataSource;
//redis存储
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
//数据库存储
@Bean
public TokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
//jwt存储
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //配置JWT使用的秘钥
accessTokenConverter.setSigningKey("123123");
return accessTokenConverter;
}
}
4.授权服务的配置
@Autowired
private TokenStore redisTokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
endpoints
.authenticationManager(authenticationManager())
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
.userDetailsService(myUserDetailsService)
.accessTokenConverter(defaultAccessTokenConverter)
.tokenEnhancer(tokenEnhancer())
.tokenStore(redisTokenStore);
;
}
完善10:配置说明
endpoints.tokenEnhancer(tokenEnhancer()) //自定义返回的token信息,比如添加Ip
endpoints.tokenStore(redisTokenStore) //token存储策略
六。踩坑系列
1.密码模式控制台报错 Encoded password does not look like BCrypt
1.数据库的密码应该和授权配置的编码器一致:为BCryptPasswordEncoder编码
2.只能声明一个BCryptPasswordEncoder,其他地方要用到直接依赖注入,不要重复声明
2.密码模式能够获取到token,但是授权码模式要获取授权码提示 403
原因是在WebSecurityConfig要配置http.httpBasic() 或者 http..formLogin()
3.刚刚获取到的token,资源服务调用却显示无效
原因是授权服务指定了toekn存储在redis,资源服务没有指定就是存储在内存
授权服务指定token存储地方
endpoints.tokenStore(redisTokenStore)
资源服务指定token存储地方
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources
.resourceId(resourceId)
.tokenStore(redisTokenStore)
;
}
注意:授权服务和资源服务要有同一个MyUserDetails
4.资源服务要和授权服务相呼应的内容
1.resourceId
2.tokenStore token存储位置 redis
3.MyUserDetails 相同文件(包名也一样)
貌似可以去掉的内容
1.注册中心
2.资源服务的配置文件 token-info-uri: http://localhost:8093/oauth/check_token #从认证授权中心上验证token
java进阶教程2天快速入门Spring Security OAuth2.0认证授权
类似微信开放平台:
1.注册应用获取appId,appSecret
2.获取code
3.通过code获取access_token
4.通过access_token获取微信用户信息
认证的两种方式:token 和 session
1.基于session的认证,认证服务器通过后把用户信息存于session
2.基于token的认证,认证服务器通过后传token给客户端
oauth2协议中的三方
我自己
服务提供商:微信、微博、QQ
第三方软件
oauth2协议中的四个角色
资源所有者 (我)
客户端/第三方应用 (APP软件)
资源服务器 (微信、微博)
授权服务器 (颁发token)
资源添加校验规则
1.授权服务器 WebSecurityConfig类里面的configure方法
2.资源服务器的方法上加注解 @PreAuthorize() @PostAuthorize() @Secured()
3.写一个类继承ResourceServerConfigurerAdapter
测试:
1.post申请令牌:http://localhost:8589/oauth/token (body填五个 拿到token)
2.
get访问资源:http://localhost:8591/api/user/manage/search/1 Headers加,Authorization : Bearer 6825a2e9-70a0-48af-b218-c5a3b6fe5eb3
post访问资源:
一。总览
Spring Security所解决的是“安全访问控制”,而安全访问控制功能其实就是对所有请求进行拦截,可以通过Filter或AOP等技术来实现,而Sping Security采用的就是Filter
如何进入认证授权:当初始化Spring Security时会创建一个名为SpringSecurityFilterChain的servlet过滤器,类型为FilterChainProxy(多个Filter链),
它实现了javax.servlet.Filter,因此外部的请求会经过此类作认证和授权
二。认证流程
a.认证流程简化版
//简化版认证流程:输入,输出,对比
1.用户提交(用户名,密码),SpringSecurity会将请求信息封装为Authentication (输入)
2.根据提交的账号查询返回UsreDetails(有可能空,有可能User) (输出) DaoAuthenticationProvider干活的人
3.通过PasswordEncoder对比Authentication的密码是否和UserDetails的密码一致 (对比)
b.源码阅读
Authentication 为客户输入,UsreDetails为查询
UsernamePasswordAuthenticationFilter 父类的doFilter方法打个断点,请求入口
DaoAuthenticationProvider retrieveUser方法打个断点
获取数据库方法:DaoAuthenticationProvider类的retrieveUser方法(其实是调用UserDetailsService接口的loadUserByUsername方法)
密码校验在方法:DaoAuthenticationProvider类的additionalAuthenticationChecks方法
根据账号查询用户:UserDetailService接口里面的loadUserByUsername UserDetailService类里面的loadUserByUsername方法(这个自定义) 表:auth_operation_user
密码编码器:在授权服务器配置文件(AuthorizationServerConfig)
//编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//对密码加密
String password= "123";
String hashpw = BCrypt.hashpw(password, BCrypt.gensalt());
//校验密码
BCrypt.checkpw("123",hashpw);
三。授权流程
在这里插入代码片
四。授权配置,即资源加校验规则
1.web授权设置 (在授权服务器)
//在授权服务器的WebSecurityConfig类里面的configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers(("/r1/r2")).hasAnyAuthority("p1") //拥有p1权限可以访问路径/r1/r2 基于资源
.antMatchers("/r1/r2").hasRole("xmfzr") //拥有角色xmfzr才可以访问路径/r1/r2 基于角色
.anyRequest().authenticated()
.and()
.cors()
.and()
.csrf().disable();
}
当把.anyRequest().permitAll()放在第二行,所有请求都会直接过去
配置规律:把详细的permitAll()放在前面,把带*的放在后面
2.方法授权设置 (在资源服务器)
//在控制器上方加注解
@PreAuthorize() @PostAuthorize() @Secured()
使用@Secured()注解还需要额外在类上加注解 @EnableGlobalMethodSecurity(securedEnabled = true)
五。Oauth2.0
下面是配置一个认证服务必须要实现的endpoints:
1.AuthorizationEndpoint用来作认证请求,默认url:/oauth/authorize
2.TokenEndpoint用作令牌的请求, 默认url: /oauth/token
3.资源服务对非法请求进行拦截,对请求中token进行解析鉴权等(Oauth2AuthenticationProcessingFilter实现对请求的token进行解析)
认证授权体系步骤:1、授权服务器,生成token; 2、资源服务器,校验服务
1。授权服务器配置:(做完就可以作颁发令牌和认证的测试)
配置什么:哪些客户端可以访问授权服务器,访问的路径,访问路径的安全约束
怎么实现:写一个类继承 AuthorizationServerConfigurerAdapter 并注解,里面三个方法巴拉巴拉
(下面资源服务器配置是继承 ResourceServerConfigurerAdapter)
1.声明授权服务器类(AuthorizationServerConfig)继承AuthorizationServerConfigurerAdapter,加注解@EnableAuthorizationServer,@Configuration
此类重写三个方法,功能分别是:
客户端详情配置(哪些客户端可以访问申请令牌)
令牌配置 (令牌访问端点和令牌怎么发放)
令牌安全配置 (哪些url可以访问颁发令牌服务,专业讲就是安全约束)
2.测试:四个模式
授权码模式:
a.申请授权码 /uaa/oauth/authorize?client_id=c1&response_ype=code&scope=all&redirect_uri=http://www.baidu.com
response_type: 必须,固定为code,表示这是一个授权码请求.
client_id: 必须,在授权服务器注册应用后得到的唯一标识(如:wxappid)
scope: 可选,请求资源范围,多个空格隔开
redirect_uri: 可选,授权码申请成功后跳转到此地址
state: 可选,如果存在,返回给客户端
b.申请令牌
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
client_id:客户端准入标识
client_secret:客户端密钥(与授权服务器三个配置中的客户端配置的secret(passwordEncoder().encode("nebulait"))一致)
grant_type:授权类型,填写authorization_code表示授权码模式(授权服务器三个配置中的客户端配置要有这个 "password","refresh_token",“authorization_code”)
code:授权码,就是钢钢获取到的授权码,注意授权码只能使用一次就无效了,需要重新申请
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uti一致
简化模式:response_type=token (客户端发送请求即返回一个token,应用于只有前端没有后端的场景下)
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
密码模式: grant_type=password (授权服务器将令牌token发给客户端,因此应用于前端自己开发的场景下)
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
client_id:客户端准入标识(授权服务器有配置)
client_secret:客户端密钥(授权服务器有配置)
grant_type=password(授权服务器有配置)
username:资源拥有者用户名
password:资源拥有者密码
客户端模式: grant_type = client_credentials
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
client_id;客户端准入标识
client_secret:客户端密钥
grant_type:填写client_credentials表示客户端模式
//授权服务器配置文件
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override //配置客户端
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory() //暂时使用内存模式
.withClient("web") //客户端id
.resourceIds(OPERATION_RESOURCE_ID, TIMED_TASK_RESOURCE_ID) //可以访问的资源列表
.authorizedGrantTypes("password", "refresh_token") //允许的授权类型
.scopes("all") //授权范围(客户端权限)
.secret(passwordEncoder().encode("nebulait")) //客户端秘钥
.accessTokenValiditySeconds(36000);//Token 的有效期 10小时
}
@Override //配置令牌 的访问端点和服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) //允许Post,get提交
.tokenStore(tokenStore())
.authenticationManager(authenticationManager) //密码模式需要
.userDetailsService(userDetailService) //autorizationCodeServive 授权码模式用这个
.accessTokenConverter(defaultAccessTokenConverter)
.tokenEnhancer(new CustomTokenEnhancer())
.tokenServices(tokenServices()) //令牌管理服务
.exceptionTranslator(new CustomWebResponseExceptionTranslator());
}
@Override //配置令牌的端点约束
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.checkTokenAccess("isAuthenticated()"); //允许表单认证
}
}
/oauth/authorize: 授权,验证身份合法性
/oauth/token: 颁发令牌(申请令牌)
/oauth/confirm_access: 确认令牌提交
/oauth/error: 错误信息
/oauth/check_token: 校验令牌
/oauth/token_key:
2。资源服务器配置(资源加校验规则)(配置完可以作资源访问的测试)
配置什么: 通俗讲就是访问路径加权限校验
怎么实现:写一个类继承 ResourceServerConfigurerAdapter ( 上面授权服务器是AuthorizationServerConfigurerAdapter,项目中没有)
1.控制层的配置
@PreAuthorize("hasanyAuthority('p1')") //拥有p1权限方可访问此url
2.写一个类继承ResourceServerConfigurerAdapter (项目无)
测试
1.get请求
heardres加 Authorization:Bearer 7930c12e-afd0-462e-9e45-b9096f09a014(token)
几种授权模式:授权码模式,密码模式,简化模式,客户端模式
六。jwt令牌
1.什么是JWT (json web token)
它是开发的行业标准,定义一种协议格式,用于通信双方传递json对象,传递信息经过数字签名可以被验证和信任。
2.为什么要有jwt令牌,能解决什么问题
当资源服务和授权服务不在一起时,资源服务使用RemoteTokenServices远程请求授权服务验证token,如果访问量较大会影响系统性能
令牌格式采用jwt即可解决上边问题:用户认证通过会得到一个Jwt令牌(有用户信息),客户端只需要携带jwt访问资源服务,资源服务根
据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。
3.jwt令牌的组成
jwt令牌有三部分,分别是头部(Header),负载(PayLoad),签名(Signature),中间用(. )分隔
Header包括令牌类型和使用的hash算法
{
"type":"JWT",
"alg":"HS256"
}
PayLoad
{
"sub":"123456",
"name":"zhasgnsan",
"admin":"true"
}
HMACSHA256{
base64URLEncode(header) + "." //令牌的第一部分
base64URLEncode(payload) + "." //令牌的第二部分
secret
}
4。java代码生成jwt格式token, 并且将token转成payLoad里面的map对象
1.pom引入jwt依赖
<!--单体使用的jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
//SpringSecurity引用的是下面的
<!‐‐spring secuity对jwt的支持,spring cloud oauth2已经依赖,可以不配置‐‐>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring‐security‐jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
2.生成token,代码见下面
3.官网 https://jwt.io,验证toekn
import io.jsonwebtoken.*;
import org.junit.Test;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Author lhc
* @Date 2021/8/13 13:39
*/
public class TestJWT {
@Test
public void test() {
String jwt = generateJWT();
Jws<Claims> claimsJws = parseJWT(jwt);
System.out.println(claimsJws);
}
private String generateJWT() {
//添加构成JWT的头部参数
Map<String, Object> headMap = new HashMap<>();
headMap.put("alg", SignatureAlgorithm.HS256.getValue());
headMap.put("typ", "JWT");
//负载
Map<String, Object> payLoadMap = new HashMap<>();
payLoadMap.put("userId", 1);
payLoadMap.put("userName", "lhccs");
payLoadMap.put("ipAddress", "localhost");
JwtBuilder builder = Jwts.builder()
//头部
.setHeader(headMap)
//负载
// .claim("userId", 1)
// .claim("userName", "test")
// .claim("ipAddress", "localhost")
.addClaims(payLoadMap)
.setId("id") //声明标识 "jti": "id",
.setSubject("subject") //声明主体 "sub": "subject",
.setIssuedAt(new Date(System.currentTimeMillis())) //声明签发时间
.setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) //声明过期时间:60s后过期
//签名
.signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(SecretConstant.BASE64SECRET.getBytes()));
String jwt = builder.compact();
System.out.println(jwt);
return jwt;
}
private Jws<Claims> parseJWT(String jwt) {
//返回的是payLoad里面的内容
Jws<Claims> claimsJws = null;
try {
claimsJws = Jwts.parser().setSigningKey(Base64.getEncoder().encodeToString(SecretConstant.BASE64SECRET.getBytes())).parseClaimsJws(jwt);
JwsHeader header = claimsJws.getHeader();
Claims body = claimsJws.getBody();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date issuedAt = body.getIssuedAt();
Date expiration = body.getExpiration();
System.out.println("签发时间:" + sdf.format(issuedAt));
System.out.println("过期时间:" + sdf.format(expiration));
System.out.println("当前时间:" + sdf.format(new Date()));
} catch (Exception e) {
e.printStackTrace();
}
return claimsJws;
}
public interface SecretConstant {
/**
* 签名秘钥 自定义
*/
String BASE64SECRET = "111111";
/**
* 超时毫秒数(默认30分钟)
*/
int EXPIRESSECOND = 3600;
/**
* 用于JWT加密的密匙 自定义
*/
String DATAKEY = "222222";
/**
* DES密钥
*/
String DESKEY = "333333";
/**
* 生产管理访问本系统的约定密钥
*/
String PRIKEY = "444444";
}
}
5。使用jwt完成的登录校验功能
1./login 根据传过来的账号密码生成token,并将token信息存储在reponse的cookie中返回给前端(前端的业务请求在header存放这个token)
String jwt = JwtUtil.generateJWT(payLoadMap); //params里面账号密码登录ip等
Cookie cookie = new Cookie("userToken", jwt);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
2. /findUser 拦截器拦截业务请求,获取请求头里面的token,根据token解析成payLoadMap,获取账号密码,判断账号密码的合法性
String jwt = request.getHeader("Authorization");
完成代码 永福的
/login
@Override
public JSONObject login(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response) {
String userName = (String) params.get("userName");
String userPwd = (String) params.get("userPwd");
JSONObject data = new JSONObject();
SysUserDto userDto = sysUserDao.getByUserName(userName);
//该用户不存在
if (userDto == null) {
log.warn("【登录验证】该用户不存在!用户名:[{}]", userName);
throw new JafException("USER_NOT_EXIST", "登录用户不存在", HttpStatus.FORBIDDEN);
}
//SHA1加密
userPwd = SHA1Util.encrytSHA1(userName, userPwd);
//密码不正确
if (!userDto.getUserPassword().equals(userPwd)) {
log.warn("【登录验证】用户密码校验不通过!!!!");
throw new JafException("USER_VERIFY_FAIL", "用户密码错误", HttpStatus.METHOD_NOT_ALLOWED);
}
String ip = (String) params.get("ipAddress");
log.info("【校验用户IP地址】IP地址为:[{}] ", ip);
params.put("userId", userDto.getId());
String jwt = JwtUtil.generateJWT(params);
Cookie cookie = new Cookie("userToken", jwt);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
data.put("token", jwt);
data.put("userInfo", userDto);
checkDoubleLogin(userDto.getId(), ip);
return data;
}
/findUser 拦截器
@Override
public boolean isPermit(HttpServletRequest request, HttpServletResponse response, Object handler) {
String jwt = request.getHeader("Authorization");
logger.info("[登录校验拦截器]-从header中获取的jwt为:{}", jwt);
//判断jwt是否有效
if (StringUtils.isNotBlank(jwt)) {
//校验jwt是否有效,有效则返回json信息,无效则返回空
String ipAddress = JwtUtil.getClientHost(request);
String resultJson = JwtUtil.validateLogin(jwt, ipAddress);
logger.info("[登录校验拦截器]-校验JWT有效性返回结果:{}", resultJson);
//resultJson为空则说明jwt超时或非法
if (StringUtils.isNotBlank(resultJson)) {
JSONObject jsonObject = JSONObject.parseObject(resultJson);
String userName = (String) ((Map<String, Object>) jsonObject).get("userName");
SysUserDto userDto = sysUserService.getByUserName(userName);
if (userDto == null) {
logger.warn("[登录校验拦截器]-登录用户不存在");
throw new JafException("USER_NOT_EXIST", "登录用户不存在", HttpStatus.UNAUTHORIZED);
}
//将userDto保存在UserContext中
UserContext.setCurrentUser(userDto);
response.setHeader("freshToken", jsonObject.getString("freshToken"));
checkIp(userDto.getId(), ipAddress);
return true;
} else {
logger.warn("[登录校验拦截器]-JWT非法或已超时,重新登录");
}
}
return false;
}
七。加入网关
1.网关主要负责的两件事:
a.作为Oauth2.0的资源服务器角色,实现接入方权限拦截,相当于上面的 二。认证流程
b.令牌解析并转发当前用户信息(明文token)给微服务
2.微服务拿到明文token也做两件事:
a.用户授权拦截(看当前用户是否有权限访问资源)
b.将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
八。星云授权服务器登录登出代码
1.登出
//关键语句: tokenServices.revokeToken(tokenId); 只要传入一个tokenValue,这个是org.springframework.security.oauth2框架爱提供的
@DeleteMapping(value = "/oauth/logout")
public ResponseEntity<Void> revokeToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenId = authorization.substring("Bearer".length() + 1);
tokenServices.revokeToken(tokenId);
}
return new ResponseEntity<>(HttpStatus.OK);
}
2.创建token 返回如下六个信息
{
“access_token”: “ce792fb3-d0e0-4198-a03f-d44257e4ef8a”,
“token_type”: “bearer”,
“refresh_token”: “dca5c7b1-5b75-4dc0-8228-6b00cc953585”,
“expires_in”: 35058,
“scope”: “all”,
“ipAddress”: “192.168.1.72”
}
* @param authUser 授权用户
* @param parameters 客户端参数
* @param authenticatedClient 授权客户端
* @param tokenRequest token请求
* @return 无密码登录流程
*/
private OAuth2AccessToken dealNoPasswordLogin(
AuthUser authUser, Map<String, String> parameters,ClientDetails authenticatedClient, okenRequest tokenRequest)
{
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(serDetailService.buildCustomUserDetails(authUser, authUser.getPhone()), null);
usernamePasswordAuthenticationToken.setDetails(parameters);
OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authenticatedClient, tokenRequest);
OAuth2AccessToken token = customTokenService.createAccessToken(
new OAuth2Authentication(storedOAuth2Request, usernamePasswordAuthenticationToken));
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return token;
}
3.登录相关代码
//传入三个参数: 1. HttpServletRequest request 2.客户端参数 3.授权时存储redis的key
a.将传入的request转成 authentication
b.将上一步的 authentication 连同传入的客户端参数 parameters 进行客户端相关验证 返回对象数组
c.根据key调用微信api 查询存储user信息并返回user
d.根据user,客户端参数,第二部的对象数据组成 OAuth2AccessToken
//a.利用传入的reqeust得到authentication
private Authentication attemptAuthentication(HttpServletRequest request)
throws AuthenticationException {
String authorization = request.getHeader("Authorization");
String clientId = "";
String clientSecret = "";
if (authorization != null && authorization.contains("Basic")) {
String clientBase64Str = authorization.substring("Basic".length() + 1);
String clientStr = new String(Base64.getDecoder()
.decode(clientBase64Str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
if (!StringUtils.isEmpty(clientStr)) {
String[] strs = clientStr.split(":");
clientId = strs[0];
clientSecret = strs[1];
}
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return authenticationManager.authenticate(authRequest);
}
//b.利用 Authentication 和客户端参数parameters 进行客户端相关验证,得到一个对象数组 object[]
/**
* 客户端相关验证
*
* @param principal 登录信息
* @param parameters 客户端参数
* @return TokenRequest 和 ClientDetails
*/
private Object[] validClient(Principal principal, @RequestParam Map<String, String> parameters) {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = clientDetailsService.loadClientByClientId(clientId);
TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
return new Object[]{tokenRequest, authenticatedClient};
}
//d。利用user,客户端参数parameters , ClientDetails, TokenRequest 得到一个token
/**
* 处理无密码登录
*
* @param authUser 授权用户
* @param parameters 客户端参数
* @param authenticatedClient 授权客户端
* @param tokenRequest token请求
* @return 无密码登录流程
*/
private OAuth2AccessToken dealNoPasswordLogin(AuthUser authUser, Map<String, String> parameters,
ClientDetails authenticatedClient, TokenRequest tokenRequest) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetailService.buildCustomUserDetails(authUser, authUser.getPhone()), null);
usernamePasswordAuthenticationToken.setDetails(parameters);
OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authenticatedClient, tokenRequest);
OAuth2AccessToken token = customTokenService.createAccessToken(
new OAuth2Authentication(storedOAuth2Request, usernamePasswordAuthenticationToken));
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return token;
}