- maven依赖
<!---基础依赖【自行添加 mysql,mybatis,web的依赖】-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
- yml
spring:
application:
name: mock_demo
redis:
host: 127.0.0.1
port: 6379
datasource:
url: jdbc:mysql://localhost:3306/user_db
username: root
password: 12345678
driver-class-name: com.mysql.jdbc.Driver
server:
port: 9898
- bean [UserInfo数据库对象 LoginUserInfo 包装userDetails的对象]
package com.kyrie.oauth2.bean;
import lombok.Data;
import java.io.Serializable;
/**
* @author:kyrie
* @date:2024/3/14 13:36
* @Description: user的bean对象
**/
@Data
public class UserInfo implements Serializable {
private static final long serialVersionUID = -8992966180137961891L;
private Long userId;
private String username;
private String password;
}
package com.kyrie.oauth2.bean;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @author:kyrie
* @date:2024/3/14 13:37
* @Description: 登录的user的bean对象
**/
@AllArgsConstructor
public class LoginUserInfo implements UserDetails {
private static final long serialVersionUID = -4823305657709121441L;
/**
* 数据库的user对象
*/
private final UserInfo userInfo;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return userInfo.getPassword();
}
@Override
public String getUsername() {
return userInfo.getUsername();
}
public UserInfo getUserInfo() {
return userInfo;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- mapper
package com.kyrie.oauth2.mapper;
import com.kyrie.oauth2.bean.UserInfo;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* @author:kyrie
* @date:2024/3/14 13:39
* @Description: user的mapper
**/
public interface UserInfoMapper {
/**
* 根据userName查询user信息
*
* @param userName 用户名
* @return user对象
*/
@Select("SELECT user_id userId,username,password FROM user WHERE username = #{userName} LIMIT 1")
UserInfo selectUserByUserName(@Param("userName") String userName);
}
- serviceImpl [实现security的UserDetailsService]
package com.kyrie.oauth2.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.kyrie.oauth2.bean.LoginUserInfo;
import com.kyrie.oauth2.bean.UserInfo;
import com.kyrie.oauth2.mapper.UserInfoMapper;
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.stereotype.Component;
/**
* @author:kyrie
* @date:2024/3/14 13:39
* @Description: 自己的登录实现
**/
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 自定义查询user信息 security会调用此方法验证user
*
* @param username 用户名
* @return UserDetails的封装对象
* @throws UsernameNotFoundException 人员信息不存在Exception
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo user = userInfoMapper.selectUserByUserName(username);
if (ObjectUtil.isNull(user)) {
throw new UsernameNotFoundException(username + " 用户信息不存在");
}
return new LoginUserInfo(user);
}
}
- controller [随便写个controller测试一下]
package com.kyrie.oauth2.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author:kyrie
* @date:2024/3/14 15:18
* @Description: 测试controller
**/
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/demo")
public String helloDemo() {
return "helloDemo";
}
}
- config [认证服务器,资源服务器,security,jwt的一些配置]
package com.kyrie.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author:kyrie
* @date:2024/3/13:46
* @Description: spring security配置
**/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注入密码加密对象
*
* @return 加密对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 注入认证管理器 用于手动验证用户 【解决无法直接注入AuthenticationManager】
*
* @return 管理对象
* @throws Exception 异常
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* HTTP请求路径权限配置
*
* @param http HttpSecurity对象
* @throws Exception 异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 可以直接访问路径
String[] whitUrlStrArray = {"/oauth/**", "/login/**", "/logout/**"};
http.csrf().disable()
.authorizeRequests()
.antMatchers(whitUrlStrArray).permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
package com.kyrie.oauth2.config;
import cn.hutool.core.util.ObjectUtil;
import com.kyrie.oauth2.bean.LoginUserInfo;
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.oauth2.common.DefaultOAuth2AccessToken;
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.redis.RedisTokenStore;
import java.util.HashMap;
import java.util.Map;
/**
* @author:kyrie
* @date:2024/3/14 13:58
* @Description: jwt的配置 【原始的token是UUID不实用 所以改为jwt格式 如果不使用jwt 没必要写这个config】
* 配置了该config 只会在原先的token基础上增强,不会替换原先的token,原先的token的字段改为了jti
**/
@Configuration
public class JwtConfig {
/**
* redis的连接工厂
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* jwt的秘钥
*/
private static final String JWT_SIGN_KEY = "demo_key";
/**
* 配置token的存储策略 TokenStore下面有很多实现类 这边直接用redis存一下
*
* @return TokenStore实例对象
*/
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
/**
* accessToken的转化器
*
* @return JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// 配置jwt使用的密钥
accessTokenConverter.setSigningKey(JWT_SIGN_KEY);
return accessTokenConverter;
}
/**
* 增加令牌的策略
*
* @return token的增强器
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return (oAuth2AccessToken, oAuth2Authentication) -> {
Map<String, Object> info = new HashMap<>(2);
info.put("demo", "demoEnhancer");
LoginUserInfo user = (LoginUserInfo) oAuth2Authentication.getPrincipal();
if (ObjectUtil.isNotNull(user)) {
if (ObjectUtil.isNotNull(user.getUserInfo())) {
info.put("userId", user.getUserInfo().getUserId());
}
}
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
};
}
}
package com.kyrie.oauth2.config;
import com.kyrie.oauth2.service.impl.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.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 java.util.ArrayList;
import java.util.List;
/**
* @author:kyrie
* @date:2024/3/14 14:13
* @Description: 认证授权服务器配置
* <p>
* /oauth/authorize:授权端点
* /oauth/token:令牌端点
* /oauth/confirm_access:用户确认授权提交端点
* /oauth/error:授权服务错误信息端点
* /oauth/check_token:用于资源服务访问的令牌解析端点
* /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话
* </p>
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailServiceImpl userDetailsService;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer tokenEnhancer;
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services) 【可以配置token增强】
*
* @param endpointsConfigurer AuthorizationServerEndpointsConfigurer
* @throws Exception 异常
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception {
// jwt增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>(2);
delegates.add(tokenEnhancer);
delegates.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(delegates);
endpointsConfigurer
.authenticationManager(authenticationManager)
// 自己的service实现类
.userDetailsService(userDetailsService)
// token的保存方式
.tokenStore(tokenStore)
// token转化器
.accessTokenConverter(jwtAccessTokenConverter)
// token增强
.tokenEnhancer(enhancerChain);
}
/**
* 配置客户端配置 【能够使用内存或JDBC方式实现获取已注册的客户端详情】
*
* @param clients ClientDetailsServiceConfigurer
* @throws Exception 异常
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 配置client-id
.withClient("demo")
// 配置client-secret
.secret(passwordEncoder.encode("123456"))
// 配置相关token的有效期
.accessTokenValiditySeconds(60 * 60 * 2)
.refreshTokenValiditySeconds(60 * 60 * 24 * 2)
// 限制某个client可以访问的资源服务 【由resource端验证】
.resourceIds("resource1")
// 配置redirect_uri,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
// 配置申请的权限范围
.scopes("all")
// 配置grant_type,表示授权类型
.authorizedGrantTypes("authorization_code", "refresh_token");
}
/**
* 配置安全配置相关信息
*
* @param security AuthorizationServerSecurityConfigurer
* @throws Exception 异常
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 使得/oauth/check_token接口可用
security.allowFormAuthenticationForClients();
security.checkTokenAccess("permitAll()");
}
}
package com.kyrie.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
/**
* @author:kyrie
* @da2024/3/14 13:57
* @Description: 资源服务器配置
**/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 【当资源服务器和授权服务器 放在一起没必要远程去调用】
*
* RemoteTokenServices中 配置了access_token的校验地址、client_id、client_secret这三个信息,
* 当用户来资源服务器请求资源时,会携带上一个access_token,通过这里的配置,就能够校验出token是否正确。
*
* @return ResourceServerTokenServices
*/
@Bean
public ResourceServerTokenServices tokenService() {
// 使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("demo");
tokenServices.setClientSecret("123456");
tokenServices.setCheckTokenEndpointUrl("http://localhost:9898/oauth/check_token");
return tokenServices;
}
/**
* 配置资源服务器
*
* @param resources ResourceServerSecurityConfigurer
* @throws Exception 异常
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
// 验证某个client能不能访问
.resourceId("resource1")
// 配置令牌服务
.tokenServices(tokenService());
}
/**
* 配置资源
*
* @param http HttpSecurity
* @throws Exception 异常
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/hello/**");
}
}
-
测试
- 先跑起来项目
- 浏览器输入 http://localhost:9898/oauth/authorize?response_type=code&client_id=demo&redirect_uri=http://www.baidu.com&scope=all [response_type -> code 授权码模式] [client_id -> 认证服务端配置的clientId] [redirect_uri -> 认证服务端配置的registeredRedirectUris]
- 输入用户名密码[数据库中的数据 也可以使用内存模式]
- 用户名密码正确后 点击同意授权 就会返回一个code
-
用postman请求获取token [先用Basic Auth 输入username 和 password 就是clientId和clientSecret,然后再body里面填写code,类型,回调地址]
-
再看redis中是否已经存上了token [因为使用的redis的store]
-
用postman调接口