一、OAuth2认证模式
一共四种认证方式,授权码模式、密码模式、简化模式和客户端模式。实现单点登录,比较流行的方法是使用jwt方式,jwt是无状态的,qit本身就能携带信息,因此服务端可以不用保存他的信息,但只要token不过期,用户就可以一直访问,这样就无法实现退出功能。如果要实现退出登录功能,就需要在服务端存储token信息,这就违背了使用jwt认证的初衷。
二、创建Security配置类
在这个类中注入需要用到的工具类以及配置放行和认证规则
/**
* SecurityConfiguration 配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 注入Redis连接工厂
@Resource
private RedisConnectionFactory redisConnectionFactory;
// Redis仓库类,初始化RedisTokenStore用于将Token存储到Redis
@Bean
public RedisTokenStore redisTokenStore(){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
redisTokenStore.setPrefix("TOKEN:"); //设置KEY的层级前缀,方便查询
return redisTokenStore;
}
// 初始化密码编辑器,用MD5加密密码
@Bean
public PasswordEncoder passwordEncoder(){
return new PasswordEncoder() {
/**
* 加密
* @param rawPassword 原始密码
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
return DigestUtil.md5Hex(rawPassword.toString());
}
/**
* 校验密码
* @param rawPassword 原始密码
* @param encodedPassword 加密密码
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return DigestUtil.md5Hex(rawPassword.toString()).equals(encodedPassword);
}
};
}
// 初始化认证管理对象,密码登录方式需要用到
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 放行和认证规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// csrf认为除了get以外的请求都是不安全的,禁用
http.csrf().disable()
.authorizeRequests()
// 放行的请求
.antMatchers("/oauth/**", "/actuator/**").permitAll()
.and()
.authorizeRequests()
// 其他请求必须认证才能访问
.anyRequest().authenticated();
}
}
三、实现自己的UserDetails接口,
/**
* 登录认证对象
*/
@Data
public class SignInIdentity implements UserDetails {
private Integer id;
private String username;
private String password;
private String roles;
private Boolean isValid;
// 角色集合
private List authorities;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
if(StrUtil.isNotBlank(this.roles)) {
// 获取数据库中的角色信息
this.authorities = Stream.of(this.roles.split(",")).map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}else{
// 如果角色为空则设置为ROLE_USER
this.authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
}
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.isValid;
}
}
四、实现自己的UserDetailsService接口
该接口提供一个loadUserByUsername方法,我们一般通过扩展这个借口来获取我们的用户信息,返回值就是上一步中定义的UserDetails。
@Service
public class UserService implements UserDetailsService {
@Resource
private DinersMapper dinersMapper;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AssertUtil.isNotEmpty(username,"请输入用户名");
Diners user = dinersMapper.selectByAccountInfo(username);
if(user == null){
throw new UsernameNotFoundException("用户名或密码错误,请重新输入");
}
// 初始化认证登录对象
SignInIdentity signInIdentity = new SignInIdentity();
BeanUtils.copyProperties(user, signInIdentity);
signInIdentity.setPassword(passwordEncoder.encode(user.getPassword()));
return signInIdentity;
}
}
五、创建认证服务器配置类
# Oauth2
client:
oauth2:
client-id: appId #客户端标识ID
secret: 123456 #客户端安全码
# 授权类型
grant-types:
- password
- refresh_token
refresh-token-validity-time: 2000
# token有效时间
token-validity-time: 3600
# 客户端访问范围
scopes:
- api
- all
/**
* 授权服务
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Resource
private ClientOauth2DataConfiguration clientOauth2DataConfiguration;
@Resource
private PasswordEncoder passwordEncoder;
// 认证管理对象
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisTokenStore redisTokenStore;
// 登录校验
@Resource
private UserService userService;
/**
* 配置令牌端点安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许访问token的公钥,默认/oauth/token_key 是受保护的
security.tokenKeyAccess("permitAll()")
// 允许检查token状态,默认/oauth/check_token 是受保护的
.checkTokenAccess("permitAll()");
}
/**
* 客户端配置 - 授权模型
* 配置被允许访问这个认证服务器的客户端信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient(clientOauth2DataConfiguration.getClientId()) // 客户端标识ID
.secret(passwordEncoder.encode(clientOauth2DataConfiguration.getSecret())) // 客户端安全码
.authorizedGrantTypes(clientOauth2DataConfiguration.getGrantTypes()) // 授权类型
.accessTokenValiditySeconds(clientOauth2DataConfiguration.getTokenValidityTime()) // token有效期
.refreshTokenValiditySeconds(clientOauth2DataConfiguration.getRefreshTokenValidityTime()) //刷新token的有效期
.scopes(clientOauth2DataConfiguration.getScopes()); // 客户端访问范围
}
/**
* 配置授权以及令牌的访问端点和令牌服务
* 授权服务器端点配置器
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 认证器
// password需要配置authenticationManager
endpoints.authenticationManager(authenticationManager)
// 具体的登录方法
.userDetailsService(userService)
// token存储方式
.tokenStore(redisTokenStore)
// 令牌增强对象,增强返回的结果
.tokenEnhancer((accessToken, authentication) -> {
SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();
LinkedHashMap map = new LinkedHashMap<>();
// 追加额外信息
map.put("username",signInIdentity.getUsername());
map.put("authorities", signInIdentity.getAuthorities());
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
token.setAdditionalInformation(map);
return token;
});
}
}
其中Authentication表示当前的认证情况,可以获取UserDetails(用户信息),Credentials(密码),isAuthenticated(是否已经认证过),Principal(用户)。Principal如果认证了,返回UserDetails,如果没有认证,就是用户名。
六、重写登录方法
/**
* Oauth2控制器
*/
@RestController
@RequestMapping("/oauth")
public class OAuthController {
@Resource
private TokenEndpoint tokenEndpoint;
@Resource
private HttpServletRequest request;
@PostMapping("/token")
public ResultInfo postAccessToken(Principal principal, @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException {
return custom(tokenEndpoint.postAccessToken(principal, parameters).getBody());
}
private ResultInfo custom(OAuth2AccessToken accessToken){
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
Map data = new LinkedHashMap(token.getAdditionalInformation());
data.put("accessToken",token.getValue());
data.put("expireIn", token.getExpiresIn());
data.put("scopes", token.getScope());
if(token.getRefreshToken() != null) {
data.put("refreshToken", token.getRefreshToken().getValue());
}
return ResultInfoUtil.buildSuccess(request.getServletPath(), data);
}
}