Spring Security OAuth2的基本使用

生活中,最使人疲惫的往往不是道路的遥远,而是心中的郁闷;最使人痛苦的往往不是生活的不幸,而是希望的破灭;最使人颓废的往往不是前途的坎坷,而是自信的丧失;最使人绝望的往往不是挫折的打击,而是心灵的死亡。所以我们要有自己的梦想,让梦想的星光指引着我们走出落漠,走出惆怅,带着我们走进自己的理想。

导读:本篇文章讲解 Spring Security OAuth2的基本使用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

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>

配置认证服务器

自定义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;
}

创建认证服务器

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

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

自定义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"));
    }
}

配置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

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

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

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

security.oauth2.client.client-id = web
security.oauth2.client.client-secret = ****

授权码模式获取令牌

向认证服务器请求授权码

http://localhost:8888/oauth/authorize?response_type=code&client_id=web&redirect_uri=http://127.0.0.1:8888/test&scope=all
response_type=code:表示授权码模式

client_id=web:配置文件中指定的web

redirect_uri:指定一个地址,用来重定向获取授权码

scope=all:表示所有权限

根据定义的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

在这里插入图片描述

请求头添加: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"
}

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

{
	"error": "invalid_grant",
	"error_description": "Invalid authorization code: E6Het7"
}

密码模式获取令牌

密码模式获取令牌相对简单 ,直接发送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"
}

配置资源服务器

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

若不配置资源服务器,直接使用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"
}

定义资源

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;
    }
}

配置application.yml

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

security:
  oauth2:
    resource:
      user-info-uri: http://127.0.0.1:8888

配置资源服务器

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

@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter{

}

执行测试

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

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

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

使用正确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"
}

注意事项

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

{
	"error": "unauthorized",
	"error_description": "Full authentication is required to access this resource"
}

解决方案:使用@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 {

}

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

使用账户、密码登录后,基于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()));
    }
}

自定义登录成功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)};
        }
    }
}

配置资源服务器

在资源服务器添加基本的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();
    }
}

执行测试

访问请求: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
}

使用令牌访问令/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"
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/136865.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!