一、 授权码模式(Authorization Code)简介
授权码模式是四种模式中最繁琐也是最安全的一种模式。
- client向资源服务器请求资源,被重定向到授权服务器(AuthorizationServer)
- 浏览器向资源拥有者索要授权,之后将用户授权发送给授权服务器
- 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client
- client拿着授权码向授权服务器索要访问令牌
- 授权服务器返回Access Token和Refresh Token给cilent。
这种模式是四种模式中最安全的一种模式。
一般用于client是Web服务器端应用或第三方的原生App调用资源服务的时候。因为在这种模式中AccessToken不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大限度的减小了AccessToken泄漏的风险。
(A)用户访问客户端,后者将前者导向认证服务器。
客户端申请认证的URI,包含以下参数:
response_type:表示授权类型,必选项,此处的值固定为"code"
client_id:表示客户端的ID,必选项
redirect_uri:表示重定向URI,可选项
scope:表示申请的权限范围,可选项
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
请求URL:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
服务器回应客户端的URI,包含以下参数:
code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
code:表示上一步获得的授权码,必选项。
redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id:表示客户端ID,必选项。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
认证服务器发送的HTTP回复,包含以下参数:
access_token:表示访问令牌,必选项。
token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
二、Maven依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
三、代码实现
- 实体类
登录用户名密码,
@Data
@AllArgsConstructor
public class User {
private long userId;
private String userName;
private String password;
}
将SpringUser类与自定义类进行转换:
import org.springframework.security.core.userdetails.User;
/**
* 自定义UserDetails类 携带User实例
*/
@Data
public class MyUserDetails extends User {
private com.xtsz.springbootauth2.entity.User user;
public MyUserDetails(com.xtsz.springbootauth2.entity.User user) {
super(user.getUserName(), user.getPassword(), true, true, true, true, Collections.EMPTY_SET);
this.user = user;
}
}
- 用户接口
继承Spring UserDetailsService 接口:
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
}
实现类:
@Primary
@Service("userService")
public class UserServiceImpl implements UserService {
private final static Set<User> users = new HashSet<>();
static {
users.add(new User(1, "admin", new BCryptPasswordEncoder().encode("123456")));
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<User> user = users.stream()
.filter((u) -> u.getUserName().equals(s))
.findFirst();
if (!user.isPresent())
throw new UsernameNotFoundException("用户不能发现!");
else
return UserDetailConverter.convert(user.get());
}
private static class UserDetailConverter {
static UserDetails convert(User user) {
return new MyUserDetails(user);
}
}
}
- Security配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* Spring Boot 2 配置,这里要bean 注入
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* 加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 认证服务配置
/**
* 认证服务配置
*/
@Configuration
@EnableAuthorizationServer
@Slf4j
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userService;
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client") // 客户端ID
.scopes("app") // 允许的授权范围
.autoApprove(true) // 如果为true 则不会跳转到授权页面,而是直接同意授权返回code
.authorizedGrantTypes("authorization_code", "refresh_token") // 设置验证方式
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin") //回调地址,也可以配置文件中定义
.secret(new BCryptPasswordEncoder().encode("123456")) //必须加密
.accessTokenValiditySeconds(10000) //token过期时间
.refreshTokenValiditySeconds(10000); //refresh过期时间;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userService); //配置userService 这样每次认证的时候会去检验用户是否锁定,有效等
}
@Bean
public TokenStore tokenStore() {
// 使用内存的tokenStore
return new InMemoryTokenStore();
}
}
注意:一定要配置回调地址。
- Security配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* Spring Boot 2 配置,这里要bean 注入
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/**
* 加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
注意:请不要配置资源服务配置
。
四、测试
-
获取令牌
获取令牌
获取令牌
获取令牌
-
接口调用
接口调用
网友评论