Spring Cloud -Hoxton.RELEASE(十四):授权中心-Spring Cloud Oauth2+JWT补全

解决几个遗留问题。
这里用上上一章《Spring Cloud -Hoxton.RELEASE(十一):注册中心/配置中心-Nacos》)的代码开始修改。
上次有人问我代码里面那些特别奇怪的代码,比如List<String> scopes = (List<String>) ((Map) ((Map) ((OAuth2Authentication) authentication).getUserAuthentication().getDetails()).get("oauth2Request")).get("scope");是怎么获取到的,因为从authentication不管怎么正常的在IDE里面调用就很难调用出这么长的包括了很多强转的代码。实际上这是用IDEA DEBUG模式下的Evaluate Expression功能推算出来的,DEBUG时候右键菜单就可以找到。虽然在IDE里没办法调用可能的强转类型的方法/变量,但是在这里就可以从authentication后面输入.就会显示出包括本身的方法/变量和推断出可能的强转类型可以调用的方法/变量。十分方便。

Spring Cloud Oauth2+JWT遗留问题

JWT增强器

以下修改都在auth项目中。
首先将之前项目生成的的JWT的playload部分(即第二部分)JTdCJTIydXNlcl9uYW1lJTIyJTNBJTIyYWRtaW4lMjIlMkMlMjJzY29wZSUyMiUzQSU1QiUyMnNlcnZlciUyMiU1RCUyQyUyMmV4cCUyMiUzQTE1Nzg2Nzg2ODglMkMlMjJhdXRob3JpdGllcyUyMiUzQSU1QiUyMmElM0FnZXQlMjIlNUQlMkMlMjJqdGklMjIlM0ElMjI5ZmJlODYyNy0wNzk4LTQ4ZWEtYjVhNi1lY2VhOGJlODMyMDIlMjIlMkMlMjJjbGllbnRfaWQlMjIlM0ElMjJjbGllbnRfMiUyMiU3RA进行Base64解码,得到内容:

{
"user_name": "admin",
"scope": ["server"],
"exp": 1578678688,
"authorities": ["a:get"],
"jti": "9fbe8627-0798-48ea-b5a6-ecea8be83202",
"client_id": "client_2"
}

可以看出里面只有六个参数,可以看出只作授权校验其实也足够,但有些情况下还是需要添加自定义的参数到JWT里面去(具体场景比较难想,就假装需要添加一个boolean的叫test的字段吧)。
添加JWT增强器:

package xyz.liuzhuoming.auth.configurer;

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 java.util.HashMap;
import java.util.Map;

/**
* jwt增强器配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> ext = new HashMap<>();
//添加自定义参数
ext.put("test", true);
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(ext);
return oAuth2AccessToken;
}
}

修改JwtConfigurer类,添加JWT增强器的Bean:

package xyz.liuzhuoming.auth.configurer;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
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;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

/**
* jwt配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
public class JwtConfigurer {

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return accessTokenConverter;
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

//添加JWT增强器的Bean
@Bean
public TokenEnhancer tokenEnhancer() {
return new JwtTokenEnhancer();
}
}

修改AuthorizationServerConfigurer:

package xyz.liuzhuoming.auth.configurer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
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 javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

/**
* 授权服务配置
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Configuration
@EnableAuthorizationServer
@Slf4j
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore tokenStore;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer tokenEnhancer;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//因为多个增强器,这里使用增强器链
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(tokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);

endpoints
.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}

@Bean
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
}

修改完成后重启auth项目。
用Postman请求token的Post请求http://localhost:5432/auth/oauth/token?username=admin&password=123456&grant_type=password(注意headerBasic Auth不能丢),得到结果:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInNlcnZlciJdLCJleHAiOjE1Nzg2ODAyMzksImF1dGhvcml0aWVzIjpbImE6Z2V0Il0sImp0aSI6ImVmZDExZTM4LWVmYjUtNDdjYy1iZmE3LTk2NzAxYzljYTU5MCIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.jkXobY18MkgyOCRPra2z0vv-UQZVZh5YOX7wMkhAGVnmWJ8z9qnd89dY-okFmMhC7EVmejkLkMYw6Gr-UET9sjE-BsjSqFKnG3D2vDzyARSBKqoEXC7GqLGofR7jRylP8KTw9YBTdOxDrvFJJdZ4Tfa8v_RUDSrNEYcn7DGdd9YLsgBv34X1IoTLucATB6cJ55sY9mjlI_bFigs_qX2zvUdsLEPFaKx55B37wrX86HNgydF-oBbo-ijiWONV-WslzFEkD-5o64uedUdDUaxf5adG_hJJXe3mopjKtpYtDEebiNyR-fq7EnIits8DZpNMT5Nt6yWQqHgAS8AOIfZQTA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInNlcnZlciJdLCJhdGkiOiJlZmQxMWUzOC1lZmI1LTQ3Y2MtYmZhNy05NjcwMWM5Y2E1OTAiLCJleHAiOjE1ODEyMjkwMzksImF1dGhvcml0aWVzIjpbImE6Z2V0Il0sImp0aSI6ImEzMDdmMmZhLTRmNTAtNDhiMi1iNjExLTFlYWVlZjMxMjYwOCIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.Raywk6MTAFlB2C5362dd3nVJUkEsaojfethPFHFfFUEFNCkqETUAmb27rmj1bgxqmyvpecjwkYKOtAteKANaoXGUJYglNMH8wLt7qYiXGzoebxeEntkImUkpb6wyvzu7foblp0RR1mzqZwGxb1VehlfULxB14wuO-P8TD7j7lKAcruWvTmV6x_ma3jnhznS_hB0kH-L09OsUmNDcDl8GYaMk9h2U85rGWf0Fn4iJnvnzBixPit5cIFXCe2Ksm7wm1zHg-z_hDlQFGQnpjqABNvKRdqlIlPTbsh538YOKv1Ei4gDxFt1zMZu63bQPmJ8Mi_M-EwsBDA4mKHNKdgfIlQ",
"expires_in": 43199,
"scope": "server",
"test": true,
"jti": "efd11e38-efb5-47cc-bfa7-96701c9ca590"
}

可以看到test参数已经出现在返回值里面了。然后像上面做的一样解析payloadeyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInNlcnZlciJdLCJleHAiOjE1Nzg2ODAyMzksImF1dGhvcml0aWVzIjpbImE6Z2V0Il0sImp0aSI6ImVmZDExZTM4LWVmYjUtNDdjYy1iZmE3LTk2NzAxYzljYTU5MCIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0,得到结果:

{
"test": true,
"user_name": "admin",
"scope": ["server"],
"exp": 1578680239,
"authorities": ["a:get"],
"jti": "efd11e38-efb5-47cc-bfa7-96701c9ca590",
"client_id": "client_2"
}

可以看到test参数也存在JWT的payload里面。说明添加JWT增强器成功。
要在client端使用自定义的增强参数需要自行解析Header里面的Authorization,很简单,在这里就不做说明了。

用户操作(增删改查)

以下修改都在auth项目中。
之前的用户名都是我自己生成之后配置在内存或者Mysql里面的,现在我们新增一个添加新用户的接口用来自己生成用户。
之前的用户密码都是通过bcrypt加密和解密的,而且相应的BeanPasswordEncoder之前已经在JwtConfigurer类写好了只是一直没用,现在我们可以把它拿出来了。
首先修改mapper,service,controller(密码加盐,接口校验,捕获异常等全部省略):
修改UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.liuzhuoming.auth.mapper.UserMapper">

<select id="selectAuthoritiesByUsername" parameterType="string"
resultType="xyz.liuzhuoming.auth.entity.Authority">
SELECT a.id,a.`name`,a.authority
FROM authority a
LEFT JOIN role_authority ra ON ra.authority_id=a.id
LEFT JOIN user_role ur ON ra.role_id=ur.role_id
LEFT JOIN `user` u ON ur.user_id=u.id
WHERE u.username=#{username}
</select>

<insert id="insertUser" parameterType="xyz.liuzhuoming.auth.entity.User">
INSERT INTO user (username,password)VALUES(#{username},#{password})
</insert>
</mapper>

修改UserMapper:

package xyz.liuzhuoming.auth.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import xyz.liuzhuoming.auth.entity.Authority;
import xyz.liuzhuoming.auth.entity.User;

import java.util.List;

/**
* 用户
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {

/**
* 根据用户名获取权限集合
*
* @param username 用户名
* @return 权限集合
*/
List<Authority> selectAuthoritiesByUsername(String username);

/**
* 新增用户
*
* @param user 用户
*/
void insertUser(User user);
}

新增UserService接口:

package xyz.liuzhuoming.auth.service.impl;

import xyz.liuzhuoming.auth.entity.User;

public interface UserService {

/**
* 新增用户
*
* @param user 用户
*/
void insertUser(User user);
}

修改UserDetailsServiceImpl实现UserService接口:

package xyz.liuzhuoming.auth.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
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 xyz.liuzhuoming.auth.entity.Authority;
import xyz.liuzhuoming.auth.entity.User;
import xyz.liuzhuoming.auth.mapper.UserMapper;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
* 用户
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService, UserService {

@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper
.eq(User::getUsername, username);
User user = userMapper.selectOne(userLambdaQueryWrapper);
List<Authority> authorityList = userMapper.selectAuthoritiesByUsername(username);
user.setAuthorities(new ArrayList<>(new HashSet<>(authorityList)));
return user;
}

@Override
public void insertUser(User user) {
//使用bcrypt加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
userMapper.insertUser(user);
}
}

修改UserController:

package xyz.liuzhuoming.auth.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.liuzhuoming.auth.entity.User;
import xyz.liuzhuoming.auth.service.impl.UserService;

import java.security.Principal;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;

@GetMapping(value = "/current")
public Principal current(OAuth2Authentication oAuth2Authentication) {
return oAuth2Authentication;
}

@PostMapping
//可以添加权限校验来防止没权限用户添加新用户,这里就不演示了
// @PreAuthorize("hasAuthority('user:insert')")
public User insert(User user) {
userService.insertUser(user);
return user;
}
}

修改完成后重启auth项目,然后用Postman的Post请求http://localhost:5432/auth/user?username=test&password=123456来添加一个用户名为test密码为123456的新用户,返回:

{
"id": null,
"username": "test",
"password": "{bcrypt}$2a$10$Hy13zqg3XsOaGkARIIryIOPWHWjS1h4fqs9yBT9Ez4WMFJ8fXMbsi",
"authorities": null,
"enabled": true,
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
}

可见密码已经正确的被bcrypt加密了。
然后用Postman获取token,修改之前的请求token的Post请求为http://localhost:5432/auth/oauth/token?username=test&password=123456&grant_type=password(注意headerBasic Auth不能丢),返回:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg2ODI5ODQsInRlc3QiOnRydWUsInVzZXJfbmFtZSI6InRlc3QiLCJqdGkiOiI3NGRmODY1Zi03YWU2LTQ1MWMtYjE3ZS0xNGZjYzYzN2UxMjYiLCJjbGllbnRfaWQiOiJjbGllbnRfMiIsInNjb3BlIjpbInNlcnZlciJdfQ.l-b06T86TiqBbSXTAvTd6J3a2HncVi7VtmPCa6ERejFoVHV1qczBzfw1Lzz4XXuU1-JQJx217mASLgNv6_FRyiLglXtn5K0wrohEDSZkbJFOm0GNauNibFn8CvwBKqDnfH97wn5AwAmdp3AAQsB3heHK-5vKsSh-0_r3Gu_209AtAmkheot63vbeu0eaD3Q_CvzE3XrrrubsGh8P9z0NeeCEd67VovcFYOMhR2kh5T2UFBxBXunGiIz7K46OcxzKz2BDVaxUjIi5WIVP1XIsOJrQcm_ssGeATWSk1WbRGlcwJvyeHsIusuQzV0Z7GSANsOTOoFDs1N_bswqYbtsWiA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJ0ZXN0Iiwic2NvcGUiOlsic2VydmVyIl0sImF0aSI6Ijc0ZGY4NjVmLTdhZTYtNDUxYy1iMTdlLTE0ZmNjNjM3ZTEyNiIsImV4cCI6MTU4MTIzMTc4NCwianRpIjoiZDM3Y2MwZDItZTMwMy00ZWRkLTk2ZmYtZGZmZTdhNWY1MDA1IiwiY2xpZW50X2lkIjoiY2xpZW50XzIifQ.bQcEpdUK613JkC0gR9LnbX04WwhV__6AqKYhQ-Q2kJhSyxS4SJxsy1yRTDRXM3mAWrXyXBDg5j-JZIpPYhpfQipDtjIhE3Ig_SkXklHCSmeLguqWea_x-9i8R3g2aioW8WBqc3s_nh3B7xn9AKDZ2rzlCwCLCzZ97NH8fWk4Q3u3u0IdwEV0wK5tBnES3zoIPudWqIDIUPUuPy21C5oy9gYR_oik0iMVuR-8m1_9IKGjwUxIh9JeeMgdalawg-PXrfNHnUZb9TzeTy4jZatnO4pVL9L0noeUzPxJb4uwXN2XRrkLy9Bb5TKXHn1fdx3IR5RnhIfwAKdIa3tDXYGHYQ",
"expires_in": 43199,
"scope": "server",
"test": true,
"jti": "74df865f-7ae6-451c-b17e-14fcc637e126"
}

用这个access_token请求http://localhost:5432/client/principle虽然新增用户没有角色也没有权限,但是这个接口也不需要权限,所以可以正常请求),返回:

{
"authorities": [],
"details": {
"remoteAddress": "192.168.0.106",
"sessionId": null,
"tokenValue": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg2ODI5ODQsInRlc3QiOnRydWUsInVzZXJfbmFtZSI6InRlc3QiLCJqdGkiOiI3NGRmODY1Zi03YWU2LTQ1MWMtYjE3ZS0xNGZjYzYzN2UxMjYiLCJjbGllbnRfaWQiOiJjbGllbnRfMiIsInNjb3BlIjpbInNlcnZlciJdfQ.l-b06T86TiqBbSXTAvTd6J3a2HncVi7VtmPCa6ERejFoVHV1qczBzfw1Lzz4XXuU1-JQJx217mASLgNv6_FRyiLglXtn5K0wrohEDSZkbJFOm0GNauNibFn8CvwBKqDnfH97wn5AwAmdp3AAQsB3heHK-5vKsSh-0_r3Gu_209AtAmkheot63vbeu0eaD3Q_CvzE3XrrrubsGh8P9z0NeeCEd67VovcFYOMhR2kh5T2UFBxBXunGiIz7K46OcxzKz2BDVaxUjIi5WIVP1XIsOJrQcm_ssGeATWSk1WbRGlcwJvyeHsIusuQzV0Z7GSANsOTOoFDs1N_bswqYbtsWiA",
"tokenType": "Bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [],
"details": {
"authorities": [],
"details": {
"remoteAddress": "192.168.0.106",
"sessionId": null,
"tokenValue": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg2ODI5ODQsInRlc3QiOnRydWUsInVzZXJfbmFtZSI6InRlc3QiLCJqdGkiOiI3NGRmODY1Zi03YWU2LTQ1MWMtYjE3ZS0xNGZjYzYzN2UxMjYiLCJjbGllbnRfaWQiOiJjbGllbnRfMiIsInNjb3BlIjpbInNlcnZlciJdfQ.l-b06T86TiqBbSXTAvTd6J3a2HncVi7VtmPCa6ERejFoVHV1qczBzfw1Lzz4XXuU1-JQJx217mASLgNv6_FRyiLglXtn5K0wrohEDSZkbJFOm0GNauNibFn8CvwBKqDnfH97wn5AwAmdp3AAQsB3heHK-5vKsSh-0_r3Gu_209AtAmkheot63vbeu0eaD3Q_CvzE3XrrrubsGh8P9z0NeeCEd67VovcFYOMhR2kh5T2UFBxBXunGiIz7K46OcxzKz2BDVaxUjIi5WIVP1XIsOJrQcm_ssGeATWSk1WbRGlcwJvyeHsIusuQzV0Z7GSANsOTOoFDs1N_bswqYbtsWiA",
"tokenType": "Bearer",
"decodedDetails": null
},
"authenticated": true,
"userAuthentication": {
"authorities": [],
"details": null,
"authenticated": true,
"principal": "test",
"credentials": "N/A",
"name": "test"
},
"principal": "test",
"oauth2Request": {
"clientId": "client_2",
"scope": [
"server"
],
"requestParameters": {
"client_id": "client_2"
},
"resourceIds": [],
"authorities": [],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": null,
"refreshTokenRequest": null
},
"credentials": "",
"clientOnly": false,
"name": "test"
},
"authenticated": true,
"principal": "test",
"credentials": "N/A",
"name": "test"
},
"credentials": "",
"clientOnly": false,
"principal": "test",
"oauth2Request": {
"clientId": null,
"scope": [],
"requestParameters": {},
"resourceIds": [],
"authorities": [],
"approved": true,
"refresh": false,
"redirectUri": null,
"responseTypes": [],
"extensions": {},
"grantType": null,
"refreshTokenRequest": null
},
"name": "test"
}

可见添加的新用户可以正常使用。
删除用户,修改用户信息,添加权限,添加角色等没难度,请自行实现。

Scope的运用

之前一直没理解Oauth2的scope是做什么用的,而client又必须提供一个scope,不然请求token的时候会报错Empty scope (either the client or the user is not allowed the requested scopes)
所以为了防止报错都是写死了一个scope值为server,但是写了又没利用到感觉十分奇怪,后来看到微信支付(或者微信公众号,具体忘记了)的Oauth2授权不同scope用作返回不同渠道的数据,突然想到scope完全可以用来做数据权限。

Scope作数据权限

首先我们先确定两个scope,分别是devtest,实际上名字不重要,只要是两个不同的就行。
然后去数据库修改oauth_client_details表的client_1的scopedev,client_2的scopetest(这两个client都是之前添加的)。
然后用Postman请求token,访问http://localhost:5432/auth/oauth/token?username=test&password=123456&grant_type=passwordBasic Auth为client_2:123456,返回结果:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg2ODQ4OTMsInRlc3QiOnRydWUsInVzZXJfbmFtZSI6InRlc3QiLCJqdGkiOiIxNTFlZDVmZi1lYWE1LTRhZjItYmU4Ny1mY2M0MmFmNmJkYjIiLCJjbGllbnRfaWQiOiJjbGllbnRfMiIsInNjb3BlIjpbInRlc3QiXX0.B11qq55IuL5pOVRA_prBofo9wpf6GFAv5PtZrcvtLtoSUSxsb2El-hjBNJhMsk1pu4BFtkoXB5BW16C_BQ5ISVWh92pq5qA63vNihNVlAqtW0YPz7smgfSzbs9KyGhSDR_LPz3XxuGdqc7L2Ayr5SfmSCJ87Gh53VaHNW-sI4zcAo_AoUdPfoJ-vsjdZPhVsSxjJ-51mahE_Jc5Ip3HA7O5MH0tQeVP1bBb7z05eNNjuc8Vnf5jd4yxJfRKpBi90OJKGMmFY-4NDxBrIaZPEW1kXLGrys50ZZKQoFHcUyD5_biEjWsqY_rAOsZZhwwj5YiLCRLAhJ1kUa9-YswBIyA",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJ0ZXN0Iiwic2NvcGUiOlsidGVzdCJdLCJhdGkiOiIxNTFlZDVmZi1lYWE1LTRhZjItYmU4Ny1mY2M0MmFmNmJkYjIiLCJleHAiOjE1ODEyMzM2OTMsImp0aSI6IjhmNWIyZjBhLWQ5NDQtNGE0Yy05OGEwLTg5ZTZmMjMxYmUyOSIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.YbpA6Xxtve9dygTqhGdL4d2DjK5fdUrLIpi-ZDEnhYayHxlbs_Hrk68LLHbaH-Pqk3YhOGkyHW6ljH4oQpmi9n0zYsY9tgXm6GGELbpw0VxEOeUkSBg8EXys6cP1Ra7xXi0Vkvj5dhkk3T1MaxhlhmgJjtRxKSNEn0Hn-_Os5EmG3YixRE8jeFsJbWv62nmQYr2jpSqwa92CMrINDlCbR2o2OFlFKbCAHD_PeD0rvDxsmMoXnvu_wgWZJZjJyHHauduaZfcq49ZlGcm5a_UulTbsCA1BkXYPP4zw-rlOemtqkN6BhblnrOzNBWDoHqn__ScK7Bi1OzYtelknSQSCyw",
"expires_in": 43199,
"scope": "test",
"test": true,
"jti": "151ed5ff-eaa5-4af2-be87-fcc42af6bdb2"
}

可见client_2的scope已经修改为test了。
然后在数据库修改client_1的authorized_grant_types字段为client_credentials,refresh_token,password,再用Postman请求token,访问http://localhost:5432/auth/oauth/token?username=test&password=123456&grant_type=passwordBasic Auth为client_1:123456,返回结果:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg2ODUwNDIsInRlc3QiOnRydWUsInVzZXJfbmFtZSI6InRlc3QiLCJqdGkiOiJlMTI3ZjBiNC1iNzQ4LTQ4YzAtYTFlYS0wZjc3NzI3MjFkYmIiLCJjbGllbnRfaWQiOiJjbGllbnRfMSIsInNjb3BlIjpbImRldiJdfQ.f6FSkVo3i7MUmqLbDMgPS4XDWNiB7TPJw2k6tMeA8waw8rxyFsQWXnb_ujMfCfGxBBXZPbHLvus-B73WfDi18WJ4x-oITbOlUFnTwtzempK58YEqwcm62VeTB0s7D4fWIOZS9YiVL1uX7BoExxQ5KMZqD_Ynj_bYiDsCLL3cKBHL0s67eAO530zy4YkfkqAmcV5UNb8FuZoHyzQD2rxo5MH0YaNbMTUgS7h2foYi4vn1uZYZmeYGw5Zh7KvvkkuDmvDLC8iMAWToxwP_ZjL_m-7MeVNeLB3a4z5a0xBAhF5pMQGufvoEG9rWk_be7qGTprgPZupdDH6EsowP0IgAjQ",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJ1c2VyX25hbWUiOiJ0ZXN0Iiwic2NvcGUiOlsiZGV2Il0sImF0aSI6ImUxMjdmMGI0LWI3NDgtNDhjMC1hMWVhLTBmNzc3MjcyMWRiYiIsImV4cCI6MTU4MTIzMzg0MiwianRpIjoiMzdkY2ViN2UtNDhhOS00ODI1LWE3Y2MtZGY0ZTU3YjQ0YmZkIiwiY2xpZW50X2lkIjoiY2xpZW50XzEifQ.A1aLYQDCAEriF5h4TeCKyFudVHUG20wp3GBIgIPdkB3V0p5UOpvG4g5UDxUfAr2o7RDdPQLIv6Laj3ZuR3PIF8X7o7MniC6YVii5iO-VrUoY7XiO7D8-PWnLx-6N1PdiA6LsQI9ym4bIsrEhkbCitwB-uY094Ack2DI1NWIrLZMcg6GmDIQ0-oIut4SnFxqfV2g8kVnWkSwX0SnTWCskd11mtcGcmU4aMYBpvXjNviV0rzWM9VImiWa_pquMBY1HZC4spJRU3sidKW9Q1n7l3XmioiWi5igOPnAus2-GwtqhnV8q9VxSd-Ytq1Qfcm3oJqlTZl267c0HyOF74K7jdQ",
"expires_in": 43199,
"scope": "dev",
"test": true,
"jti": "e127f0b4-b748-48c0-a1ea-0f7772721dbb"
}

可见client_1的scope已经修改为dev了。
然后修改client项目的TestController为:

package xyz.liuzhuoming.client.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* 测试
*
* @author liuzhuoming
* @version 1.0-SNAPSHOT
*/
@RestController
@Slf4j
@RefreshScope
public class TestController {

@GetMapping("/a")
@PreAuthorize("hasAuthority('a:get')")
public String a() {
return "a";
}

@GetMapping("/b")
@PreAuthorize("hasAuthority('b:get')")
public String b() {
return "b";
}

@GetMapping("/principle")
public Authentication principle(Authentication authentication) {
return authentication;
}

//根据scope返回对应数据
@GetMapping("/data")
@SuppressWarnings({"rawtypes", "unchecked"})
public List<String> data(Authentication authentication) {
//从Authentication获取scope集合
List<String> scopes = (List<String>) ((Map) ((Map) ((OAuth2Authentication) authentication).getUserAuthentication().getDetails()).get("oauth2Request")).get("scope");
List<String> list = new ArrayList<>(scopes.size());

for (String scope : scopes) {
switch (scope) {
case "test":
list.add("11111");
break;
case "dev":
list.add("123456");
break;
}
}
return list;
}
}

然后用client_2请求token后在Postman带token请求http://localhost:5432/client/data,返回:

[
"11111"
]

然后用client_1请求token后在Postman带token请求http://localhost:5432/client/data,返回:

[
"123456"
]

可见正确的进行了不同数据权限的区分。

Oauth2的Client操作(增删改查)

加密方式同用户模块。略