之前公司大佬让我用oauth 2的标准开发一套资源授权系统。 阅读了很多资料,部分展示在参考资料那趴。 因为spring security oauth 2.0设计的权限管理表格比较复杂,所以楼主实现了自定义用户表、客户端表。此版本缺少权限分配。
资源
- code repository
- api doc
技术栈
- spring boot 2.0
- spring security oauth 2.0
- spring jpa
- keytool
- jwt
- mysql
开发
一、架构
这是我习惯的架构方式,根据爱好自己分配。
二、依赖工具
- lombok
- 参考文档:https://blog.csdn.net/zhglance/article/details/54931430
- 阿里代码规范插件(非必须)
- 参考文档:https://github.com/alibaba/p3c
三、基础项目配置
1. 跨域
1)在配置文件中添加类,CorsFilter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = ((HttpServletResponse) servletResponse);
HttpServletRequest request = (HttpServletRequest) servletRequest;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods",
"ACL, CANCELUPLOAD, CHECKIN, CHECKOUT, COPY, DELETE, GET, HEAD, LOCK, MKCALENDAR, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, REPORT, SEARCH, UNCHECKOUT, UNLOCK, UPDATE, VERSION-CONTROL");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// not needed
}
@Override
public void destroy() {
// not needed
}
}
由于三次握手,options的请求方式要直接通过。即:在正式跨域的请求前,浏览器会根据需要,发起一个“PreFlight”(也就是Option请求),用来让服务端返回允许的方法(如get、post),被跨域访问的Origin(来源,或者域),还有是否需要Credentials(认证信息
2. 异常处理
1)自定义异常的类型
@Getter
public enum ResultEnum {
#示例
SUCCESS(100, "返回成功"),
MISS_REQUEST_PARAM_ERROR(101, "缺少请求参数"),
SAVE_USERNAME_EXIST_ERROR(102, "用户已存在!"),
SAVE_CLIENT_EXIST_ERROR(103, "客户端已存在!"),
PUBLIC_KEY_NOT_EXIST(104, "公钥不存在!")
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
2)同一定义返回的vo对象
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
/** 错误码 */
private Integer code;
/** 提示信息 */
private String msg;
/** 具体内容 */
private T data;
}
3)自定义VO工具
public class ResultUtil {
public static Result success(Object object) {
Result
3)自定义异常
@Getter
public class MyException extends RuntimeException {
private Integer code;
public MyException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
public MyException(Integer code, String msg) {
super(msg);
this.code = code;
}
public MyException(ResultEnum resultEnum, String msg) {
super(resultEnum.getMessage().concat(msg));
this.code = resultEnum.getCode();
}
}
4)自定义异常处理
@Slf4j
@ControllerAdvice
public class ExceptionHandle {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result handle(Exception e) {
if (e instanceof MyException) {
MyException myException = (MyException) e;
log.error(myException.toString());
return ResultUtil.error(myException.getCode(), e.getMessage());
} else if (e instanceof MissingServletRequestParameterException) {
return ResultUtil.error(ResultEnum.MISS_REQUEST_PARAM_ERROR);
} else {
log.error("【系统异常】{}", e.toString());
e.printStackTrace();
return ResultUtil.error(-1, e.getMessage());
}
}
}
至此,项目的基础工具搭建完毕。
四、授权服务器配置 (Authorization Server)
1)原理2)数据库表设计
出于方便维护的出发点,这里使用spring data jpa的方式来生成并管理表
#用户表
@Data
@Entity
@Table(name = "oauth_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 用户名 */
@Column(nullable = false, unique = true)
private String username;
/** 密码 */
private String password;
/** 是否可用 */
private Boolean enabled;
/** 是否被锁 */
private Boolean noLocked;
}
@Data
@Entity
@Table(name = "oauth_client")
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "client_id", unique = true)
private String clientId;
private String resourceIds;
private String clientSecret;
private Boolean secretRequire;
private String scope;
private Boolean scopeRequire;
private String authorizedGrantTypes;
private String authorities;
private Integer accessTokenValidity;
private Integer refreshTokenValidity;
}
当然,如果你喜欢用sql文件来管理,那么将是如下:
-- table for client details
DROP TABLE IF EXISTS `oauth_client`;
CREATE TABLE `oauth_client` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(256) UNIQUE NOT NULL COMMENT 'store the id of newly registered clients',
resource_ids VARCHAR(256) NOT NULL COMMENT 'store the password of clients',
client_secret VARCHAR(256) DEFAULT '' COMMENT 'which indicates if client is still valid',
secret_require BIT NOT NULL DEFAULT 1,
scope VARCHAR(256) DEFAULT '' COMMENT 'allowed actions, for example writing statuses on Facebook etc.',
scope_require BIT NOT NULL DEFAULT 1,
authorized_grant_types VARCHAR(256) NOT NULL COMMENT 'which provides information how users can login to the particular client (in our example case it’s a form login with password)',
authorities VARCHAR(256) NOT NULL COMMENT '授权',
access_token_validity INTEGER COMMENT '',
refresh_token_validity INTEGER COMMENT ''
);
-- user
DROP TABLE IF EXISTS `oauth_user`;
CREATE TABLE `oauth_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id),
`username` VARCHAR(255) UNIQUE NOT NULL,
`password` VARCHAR(255),
`enabled` BIT NOT NULL DEFAULT 1 COMMENT '是否有效',
`no_locked` BIT NOT NULL DEFAULT 1 COMMENT '是否锁定账户'
);
3)repository(mapper) 配置
楼主用的jpa,所以直接继承JpaRepository。或者也可以用mybatis的mapper映射数据库的数据。
public interface ClientRepository extends JpaRepository {
/**
* 按clientId查找client
* @param clientId
* @return
*/
Client findByClientId(String clientId);
}
public interface UserRepository extends JpaRepository {
/**
* 按名称查询用户
* @param username
* @return
*/
User findByUsername(String username);
}
4)定义principal
定义clientPrincipal和userPrincipal用于spring security的数据读取、使用。
public class MyUserPrincipal implements UserDetails {
//TODO:等待完善权限分配...
private User user;
public MyUserPrincipal(User user) {
this.user = user;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return user.getNoLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getEnabled();
}
}
public class MyClientPrincipal implements ClientDetails {
//TODO:等待完善...
private Client client;
public MyClientPrincipal(Client client) {
this.client = client;
}
@Override
public String getClientId() {
return client.getClientId();
}
@Override
public Set getResourceIds() {
return new HashSet<>(Arrays.asList(client.getResourceIds().split(Constant.SPLIT_COMMA)));
}
@Override
public boolean isSecretRequired() {
return client.getSecretRequire();
}
@Override
public String getClientSecret() {
return client.getClientSecret();
}
@Override
public boolean isScoped() {
return client.getScopeRequire();
}
@Override
public Set getScope() {
return new HashSet<>(Arrays.asList(client.getScope().split(Constant.SPLIT_COMMA)));
}
@Override
public Set getAuthorizedGrantTypes() {
return new HashSet<>(Arrays.asList(client.getAuthorizedGrantTypes().split(Constant.SPLIT_COMMA)));
}
@Override
public Set getRegisteredRedirectUri() {
return null;
}
@Override
public Collection getAuthorities() {
Collection collection = new ArrayList<>();
Arrays.asList(client.getAuthorities().split(Constant.SPLIT_COMMA)).forEach(
auth -> collection.add((GrantedAuthority) () -> auth)
);
return collection;
}
@Override
public Integer getAccessTokenValiditySeconds() {
return client.getAccessTokenValidity();
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return client.getRefreshTokenValidity();
}
@Override
public boolean isAutoApprove(String scope) {
return false;
}
@Override
public Map getAdditionalInformation() {
return null;
}
}
5)service层
自定义service继承spring security的UserDetailsService和ClientDetailsService。目的是使用自定义的mysql表管理用户和客户端。
它们的实现如下:
@Service
public class MyUserDetailsServiceImpl implements MyUserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new MyUserPrincipal(user);
}
@Override
public User save(User user) {
//1、如果已存在,则抛出异常
if (userRepository.findByUsername(user.getUsername()) != null) {
throw new MyException(ResultEnum.SAVE_USERNAME_EXIST_ERROR);
}
//2、否则,添加
return userRepository.save(user);
}
}
@Service
public class MyClientDetailsServiceImpl implements MyClientDetailsService {
@Autowired
private ClientRepository clientRepository;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
Client client = clientRepository.findByClientId(clientId);
if (client == null) {
throw new ClientRegistrationException(clientId);
}
return new MyClientPrincipal(client);
}
@Override
public Client save(Client client) {
//1、如果已存在,抛出异常
if (clientRepository.findByClientId(client.getClientId()) != null) {
throw new MyException(ResultEnum.SAVE_CLIENT_EXIST_ERROR);
}
//2、否则,添加
return clientRepository.save(client);
}
}
6)controller层
passwordEncoder先不用管,后续会添加到bean中。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private MyUserDetailsService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@RequestMapping("/create")
public User create(@RequestParam String username,
@RequestParam String password) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
user.setNoLocked(true);
return userService.save(user);
}
}
@RestController
@RequestMapping("/client")
public class ClientController {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private MyClientDetailsService myClientDetailsService;
@RequestMapping("/create")
public Client create(@RequestParam String clientId,
@RequestParam String resourceIds,
@RequestParam(required = false, defaultValue = "") String clientSecret,
@RequestParam String scope,
@RequestParam String authorities,
@RequestParam String authorizedGrantTypes,
@RequestParam(required = false, defaultValue = Constant.DEFAULT_ACCESS_TOKEN_VALIDITY) Integer accessTokenValidity,
@RequestParam(required = false, defaultValue = Constant.DEFAULT_REFRESH_TOKEN_VALIDITY) Integer refreshTokenValidity
) {
Client client = new Client();
client.setClientId(clientId);
client.setResourceIds(resourceIds);
client.setClientSecret(passwordEncoder.encode(clientSecret));
client.setSecretRequire(!"".equals(clientSecret));
client.setScope(scope);
client.setScopeRequire(!"".equals(scope));
client.setAuthorities(authorities);
client.setAuthorizedGrantTypes(authorizedGrantTypes);
client.setAccessTokenValidity(accessTokenValidity);
client.setRefreshTokenValidity(refreshTokenValidity);
return myClientDetailsService.save(client);
}
}
#测试使用
@RestController
@RequestMapping("/order")
public class TestController {
@GetMapping("/test")
public Map test(OAuth2Authentication auth) {
Map result = new HashMap<>(3);
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
result.put("userId", auth.getName());
result.put("userIP", details.getRemoteAddress());
result.put("userRole", auth.getAuthorities());
return result;
}
}
7)配置文件
基本的spring security的配置,值得注意的是spring security 5.0后authenticationManager需要手动注入到bean里面:
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
/** 将AuthenticationManager暴露到bean中 */
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
}
授权服务器自身的资源服务器配置(为了管理用户和客户端的增删改查):
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(Constant.RESOURCE_ID_NORMAL_APP)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().anyRequest()
.and()
//允许匿名登陆
.anonymous()
.and()
.authorizeRequests()
// .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
//配置order访问控制,必须认证过后才可以访问
.antMatchers("/user/**").permitAll()
.antMatchers("/order/**").authenticated();
}
}
授权服务器配置,关于其中密钥文件,生成方法详见从头开始spring security oauth 2.0 (二):
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MyClientDetailsService clientDetailsService;
/**
* 客户端访问配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
.tokenStore(tokenStore())
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
.userDetailsService(userDetailsService);
}
/**
* 配置客户端详情
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
/**
* 配置AuthorizationServer安全认证的相关信息
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
// -------------次要配置⚠️每个资源服务都需要配置以下内容!!!-------------------
/**
* 加密方式
* @return
*/
@Bean
public static PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map encoders = new HashMap<>(1);
encoders.put(idForEncode, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
/**
* 配置AccessToken的存储方式、
* 可选择的存储方式是:
* 1、InMemoryTokenStore
* 2、JdbcTokenStore
* 3、JwtTokenStore
* 4、RedisTokenStore
* 5、JwkTokenStore
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* token converter
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
/*
* 授权服务配置
*/
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("jwt-key.jks"),
"wcm520".toCharArray()
);
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("wcm-key"));
/*
* 对应的资源服务配置
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new MyException(ResultEnum.PUBLIC_KEY_NOT_EXIST);
}
accessTokenConverter.setVerifier(publicKey);
*/
return accessTokenConverter;
}
}
参考资料
- 阮一峰 - 理解OAuth 2.0
- csdn - 下一秒升华 - 从零开始的Spring Security Oauth2