• Spring Security OAuth2的基本使用


    Spring Security OAuth2

    Spring Security OAuth2对Oauth2进行了实现,主要包含认证服务器和资源服务器两大块的实现。

    认证服务器

    对四种授权模式的实现和Token的生成与存储,也可自定义获取Token的方式

    资源服务器

    在Spring Security过滤器链上加OAuth2AuthenticationProcessingFilter过滤器,让OAuth2协议发放令牌认证的方式来保护资源

    添加依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
            </dependency>
    	
    	 <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Hoxton.SR8</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    配置认证服务器

    自定义MyUser对象

    @Data
    public class MyUser implements Serializable {
        /**
         * 用户名
         */
        private String userName;
        /**
         * 密码
         */
        private String password;
        /**
         * 账号是否未过期
         */
        private boolean accountNonExpired = true;
        /**
         * 账号是否未锁定
         */
        private boolean accountNonLocked = true;
        /**
         * 凭证是否未过期
         */
        private boolean credentialsNonExpired = true;
        /**
         * 账号是否启用
         */
        private boolean enabled = true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    创建认证服务器

    在Spring Security的配置类上使用@EnableAuthorizationServer注解申明

    @Configuration
    @EnableAuthorizationServer
    public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自定义MyUserDetailService

    自定义MyUserDetailService,实现账号登录相关校验逻辑,预设密码必须为123456,且拥有admin权限,使用任意账号即可登录。

    @Service
    public class MyUserDetailService implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            MyUser user = new MyUser();
            user.setUserName(username);
            user.setPassword(this.passwordEncoder.encode("123456"));
            return new User(username, user.getPassword(), user.isEnabled(),
                    user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                    user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    配置application.yml

    若不配置指定client-id和client-secret,则会随机分配

    security:
      oauth2:
        client:
          client-id: web
          client-secret: 123456789
          registered-redirect-uri: http://127.0.0.1:8888/test
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    启动项目,控制台打印出随机分配的client-id和client-secret

    security.oauth2.client.client-id = 798f97b8-e9b0-4c87-88c2-6b35b9813966
    security.oauth2.client.client-secret = 782fa08a-bec5-4d27-8269-7f7b35488143
    
    • 1
    • 2

    若配置指定,则打印指定的参数

    security.oauth2.client.client-id = web
    security.oauth2.client.client-secret = ****
    
    • 1
    • 2

    授权码模式获取令牌

    向认证服务器请求授权码

    http://localhost:8888/oauth/authorize?response_type=code&client_id=web&redirect_uri=http://127.0.0.1:8888/test&scope=all
    
    • 1
    response_type=code:表示授权码模式
    
    client_id=web:配置文件中指定的web
    
    redirect_uri:指定一个地址,用来重定向获取授权码
    
    scope=all:表示所有权限
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    根据定义的UserDetailService逻辑,使用任意用户名,密码123456登录即可
    在这里插入图片描述
    注意:指定的redirect_uri必须同时在配置文件中指定,否则出现以下错误提示

    在这里插入图片描述
    登录成功后页面成功跳转到授权页面
    在这里插入图片描述
    选择Approve,点击Authorize按钮后,页面跳转到指定的redirect_uri,并带上授权码信息
    在这里插入图片描述
    通过授权码CODE从认证服务器获取令牌Token

    grant_type:authorization_code
    
    code:授权码
    
    client_id:web
    
    redirect_uri:http://127.0.0.1:8888/test
    
    scope:all
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    请求头添加:key=Authorization,value=Basic加上client_id:client_secret经过base64加密后的值

    base64:https://1024tools.com/base64

    在这里插入图片描述
    在这里插入图片描述
    确认参数无误后,发送请求,获取令牌Token

    {
    	"access_token": "882698ab-7a08-4aaf-847c-11c2d191540d",
    	"token_type": "bearer",
    	"refresh_token": "ea709f2f-987a-4ecf-ae79-62267a453a10",
    	"expires_in": 43199,
    	"scope": "all"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:一个授权码只能换一次令牌,如果再次使用相同授权码Code获取令牌,将返回如下信息

    {
    	"error": "invalid_grant",
    	"error_description": "Invalid authorization code: E6Het7"
    }
    
    • 1
    • 2
    • 3
    • 4

    密码模式获取令牌

    密码模式获取令牌相对简单 ,直接发送Post请求:http://localhost:8888/oauth/token
    在这里插入图片描述
    请求头添加:key=Authorization,value=Basic加上client_id:client_secret经过base64加密后的值
    在这里插入图片描述
    发送请求,获取令牌

    {
    	"access_token": "5983f2eb-d83d-4d5a-8e94-89cd417d360f",
    	"token_type": "bearer",
    	"refresh_token": "56b1d244-d5d5-4f3c-8597-7921f6d6c03b",
    	"expires_in": 43199,
    	"scope": "all"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    配置资源服务器

    配置资源服务器,让客户端可以通过合法的令牌来获取定义的资源。

    若不配置资源服务器,直接使用Token(请求头添加key=Authorization,值=token_type access_token)访问资源,即使Token正确,也无法访问资源信息。将返回如下信息:

    {
    	"timestamp": "2022-10-20T07:06:26.045+00:00",
    	"status": 401,
    	"error": "Unauthorized",
    	"message": "Unauthorized",
    	"path": "/index"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    定义资源

    import org.springframework.security.core.Authentication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
    
        @GetMapping("index")
        public Object index(Authentication authentication) {
            return authentication;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    配置application.yml

    user-info-uri指定认证服务器地址和端口

    security:
      oauth2:
        resource:
          user-info-uri: http://127.0.0.1:8888
    
    • 1
    • 2
    • 3
    • 4

    配置资源服务器

    在配置类上使用@EnableResourceServer注解申明是一个资源服务器

    @Configuration
    @EnableResourceServer
    public class MyResourceServerConfig extends ResourceServerConfigurerAdapter{
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    执行测试

    使用授权模式或者密码模式获取令牌,然后使用令牌请求资源地址:http://localhost:8888/index,注意携带请求头。

    在这里插入图片描述
    使用错误access_token

    {
    	"error": "invalid_token",
    	"error_description": "Invalid access token: 1d4379313-c2f3-4ba4-87ce-26371726ba62"
    }
    
    • 1
    • 2
    • 3
    • 4

    使用正确access_token

    {
    	"authorities": [
    		{
    			"authority": "admin"
    		}
    	],
    	"details": {
    		"remoteAddress": "127.0.0.1",
    		"sessionId": null,
    		"tokenValue": "d4379313-c2f3-4ba4-87ce-26371726ba62",
    		"tokenType": "Bearer",
    		"decodedDetails": null
    	},
    	"authenticated": true,
    	"userAuthentication": {
    		"authorities": [
    			{
    				"authority": "admin"
    			}
    		],
    		"details": {
    			"grant_type": "password",
    			"username": "admin",
    			"scope": "all"
    		},
    		"authenticated": true,
    		"principal": {
    			"password": null,
    			"username": "admin",
    			"authorities": [
    				{
    					"authority": "admin"
    				}
    			],
    			"accountNonExpired": true,
    			"accountNonLocked": true,
    			"credentialsNonExpired": true,
    			"enabled": true
    		},
    		"credentials": null,
    		"name": "admin"
    	},
    	"principal": {
    		"password": null,
    		"username": "admin",
    		"authorities": [
    			{
    				"authority": "admin"
    			}
    		],
    		"accountNonExpired": true,
    		"accountNonLocked": true,
    		"credentialsNonExpired": true,
    		"enabled": true
    	},
    	"oauth2Request": {
    		"clientId": "web",
    		"scope": [
    			"all"
    		],
    		"requestParameters": {
    			"grant_type": "password",
    			"username": "admin",
    			"scope": "all"
    		},
    		"resourceIds": [],
    		"authorities": [
    			{
    				"authority": "ROLE_USER"
    			}
    		],
    		"approved": true,
    		"refresh": false,
    		"redirectUri": null,
    		"responseTypes": [],
    		"extensions": {},
    		"grantType": "password",
    		"refreshTokenRequest": null
    	},
    	"clientOnly": false,
    	"credentials": "",
    	"name": "admin"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    注意事项

    若同时定义认证服务器和资源服务器,使用授权码模式获取令牌可能会遇到异常:

    {
    	"error": "unauthorized",
    	"error_description": "Full authentication is required to access this resource"
    }
    
    • 1
    • 2
    • 3
    • 4

    解决方案:使用@Order()注解,只要确保认证服务器先于资源服务器配置即可

    @Order(1)
    @Configuration
    @EnableAuthorizationServer
    public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
    @Order(2)
    @Configuration
    @EnableResourceServer
    public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    自定义账号密码登录获取令牌

    使用账户、密码登录后,基于Spring Security OAuth2默认配置生成相应Token令牌。

    自定义登录失败Handler

    创建MyAuthenticationFailureHandler 类实现AuthenticationFailureHandler接口,重写onAuthenticationFailure方法,添加处理登录失败逻辑。

    import com.alibaba.fastjson.JSONObject;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
            //  设置响应状态码
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            // 设置响应内容格式
            response.setContentType("application/json;charset=utf-8");
            // 设置响应内容
            response.getWriter().write(JSONObject.toJSONString(exception.getMessage()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    自定义登录成功Handler

    创建MyAuthenticationSucessHandler类实现AuthenticationSuccessHandler接口,重写onAuthenticationSuccess方法,添加处理登录成功逻辑,主要是生产Token令牌的过程。

    @Component
    @Slf4j
    public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    
        @Autowired
        private ClientDetailsService clientDetailsService;
        @Autowired
        private AuthorizationServerTokenServices authorizationServerTokenServices;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
            // 从请求头中获取ClientId
            String header = request.getHeader("Authorization");
            if (header == null || !header.startsWith("Basic ")) {
                throw new BadCredentialsException("凭证有误");
            }
    
            String[] tokens = this.decoding(header);
            String clientId = tokens[0];
            String clientSecret = tokens[1];
    
            TokenRequest tokenRequest;
            // 使用ClientDetailsService获取ClientDetails
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
    
            // 校验ClientId和ClientSecret的正确性
            if (clientDetails == null) {
                throw new UnapprovedClientAuthenticationException("客户端 clientId:" + clientId + "对应信息不存在");
            } else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
                throw new UnapprovedClientAuthenticationException("客户端 Secret 不正确");
            } else {
                // 使用TokenRequest构造器生成TokenRequest
                tokenRequest = new TokenRequest(new HashMap<>(), clientId, clientDetails.getScope(), "custom");
            }
    
            // 使用createOAuth2Request方法获取OAuth2Request
            OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
            // 构造OAuth2Authentication
            OAuth2Authentication auth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
            // 生成OAuth2AccessToken
            OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(auth2Authentication);
            // 返回Token
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(token));
        }
    
        private String[] decoding(String header) {
            // Basic d2ViOjEyMzQ1Njc4OQ==进行处理
            byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
            // 进行解码
            byte[] decoded = Base64.getDecoder().decode(base64Token);
            // 转字符串,得到 web:123456789
            String token = new String(decoded, StandardCharsets.UTF_8);
            int res = token.indexOf(":");
            if (res == -1) {
                throw new BadCredentialsException("凭证有误");
            } else {
                return new String[]{token.substring(0, res), token.substring(res + 1)};
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    配置资源服务器

    在资源服务器添加基本的Spring Security配置

    @Configuration
    @EnableResourceServer
    public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private MyAuthenticationSucessHandler authenticationSucessHandler;
        @Autowired
        private MyAuthenticationFailureHandler authenticationFailureHandler;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.formLogin() // 表单登录
                    .loginProcessingUrl("/login") // 表单登录URL
                    .successHandler(authenticationSucessHandler) // 处理登录成功
                    .failureHandler(authenticationFailureHandler) // 处理登录失败
                    .and()
                    .authorizeRequests() // 授权配置
                    .anyRequest()  // 所有请求
                    .authenticated() // 都需要认证
                    .and()
                    .csrf().disable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    执行测试

    访问请求:http://localhost:8888/login接口
    在这里插入图片描述

    请求头添加:key=Authorization,value=Basic加上client_id:client_secret经过base64加密后的值

    base64编码:https://1024tools.com/base64
    在这里插入图片描述
    发送POST请求,成功获取到令牌

    {
    	"access_token": "84d19e68-9975-4182-a048-09c2b551f343",
    	"token_type": "bearer",
    	"refresh_token": "bfee81a9-bb53-4812-a50d-6b6a9488a00f",
    	"expires_in": 43197
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用令牌访问令/index接口

    在这里插入图片描述

    {
    	"authorities": [
    		{
    			"authority": "admin"
    		}
    	],
    	"details": {
    		"remoteAddress": "127.0.0.1",
    		"sessionId": null,
    		"tokenValue": "ce5b625f-b2c9-444c-b2cd-2ed86b16a625",
    		"tokenType": "Bearer",
    		"decodedDetails": null
    	},
    	"authenticated": true,
    	"userAuthentication": {
    		"authorities": [
    			{
    				"authority": "admin"
    			}
    		],
    		"details": {
    			"remoteAddress": "127.0.0.1",
    			"sessionId": null
    		},
    		"authenticated": true,
    		"principal": {
    			"password": null,
    			"username": "test_user",
    			"authorities": [
    				{
    					"authority": "admin"
    				}
    			],
    			"accountNonExpired": true,
    			"accountNonLocked": true,
    			"credentialsNonExpired": true,
    			"enabled": true
    		},
    		"credentials": null,
    		"name": "test_user"
    	},
    	"principal": {
    		"password": null,
    		"username": "test_user",
    		"authorities": [
    			{
    				"authority": "admin"
    			}
    		],
    		"accountNonExpired": true,
    		"accountNonLocked": true,
    		"credentialsNonExpired": true,
    		"enabled": true
    	},
    	"oauth2Request": {
    		"clientId": "web",
    		"scope": [],
    		"requestParameters": {
    			"grant_type": "custom"
    		},
    		"resourceIds": [],
    		"authorities": [
    			{
    				"authority": "ROLE_USER"
    			}
    		],
    		"approved": true,
    		"refresh": false,
    		"redirectUri": null,
    		"responseTypes": [],
    		"extensions": {},
    		"grantType": "custom",
    		"refreshTokenRequest": null
    	},
    	"clientOnly": false,
    	"credentials": "",
    	"name": "test_user"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
  • 相关阅读:
    jenkins中执行docker命令
    序列化技术ProtoBuf
    tkwebview2创作心得
    不同图框架的差异性
    又现信息泄露事件!融云揭秘通讯安全守护之道
    【Redis底层解析】跳跃表
    Pandas读取json文件
    element ui框架(路由参数传递)
    CLR C#--计算型异步操作
    神经网络深度学习(四)特征处理
  • 原文地址:https://blog.csdn.net/qq_38628046/article/details/127425946