目录
一、前言
1、客户端详情表oauth_client_details
client_id:客户端ID
resources_ids:可访问的资源服务器ID,不写则不校验
client_secret:客户端密码,此处不能是明文,需要加密
scope:客户端授权范围,不指定默认不校验范围
authorized_grant_types:客户端授权类型,支持多个使用逗号分隔。(authorization_code,password,implicit,client_credentials,refresh_token)
web_server_redirect_uri:服务器回调地址
autoapprove:false:显示授权点击页,true:不显示自动授权
2、默认提供的令牌访问端点
/oauth/authorize:申请授权码code
/oauth/token:获取令牌token
/oauth/check_token:用于资源服务器请求端点来检查令牌是否有效
/oauth/confirm_access:用户确认授权提交
/oauth/error:授权服务错误信息
/oauth/token_key:提供公有密钥的端点,使用JWT令牌时会使用
3、授权码模式流程
1、获取授权码code:
浏览器输入:http://localhost:7001/auth/oauth/authorize?client_id=yyy&response_type=code
用户登录
用户授权:autoapprove为true则跳过授权
携带code跳转到web_server_redirect_uri:https://www.xxx.com/?code=gEAogD
2、获取token
POST http://localhost:7001/auth/oauth/token
Authorization: Basic
Username=client_id
Password=client_secret
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=gEAogD
二、代码
1、安全配置类
package com.wuxi.project.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* 安全配置类
*/
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl);
}
@Bean // 密码模式需要使用
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2、认证服务器配置
package com.wuxi.project.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer // 标识为认证服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置被允许访问此认证服务器的客户端信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// jdbc管理客户端
clients.withClientDetails(jdbcClientDetailsService());
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsServiceImpl;
@Autowired
private TokenStore tokenStore;
@Bean
public AuthorizationCodeServices jdbcAuthorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 关于认证服务器端点配置
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 密码模式需要使用
endpoints.authenticationManager(authenticationManager);
// 刷新令牌需要使用
endpoints.userDetailsService(userDetailsServiceImpl);
// 令牌的管理方式
endpoints.tokenStore(tokenStore);
// 授权码管理策略
endpoints.authorizationCodeServices(jdbcAuthorizationCodeServices());
}
/**
* 令牌端点的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 认证后可访问 /oauth/token_key , 默认拒绝访问
security.tokenKeyAccess("permitAll()");
// 认证后可访问 /oauth/check_token , 默认拒绝访问
security.checkTokenAccess("isAuthenticated()");
}
}
3、Token配置
package com.wuxi.project.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
public class TokenConfig {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
// 在使用Jwt管理令牌时,出现了用户无法授权范围的问题,还未能解决
return new JdbcTokenStore(dataSource);
}
}
4、PasswordEncoder配置
package com.wuxi.project.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5、UserDetailsService
package com.wuxi.project.auth.service;
import com.wuxi.project.auth.entities.JwtUser;
import com.wuxi.project.common.entities.SysMenu;
import com.wuxi.project.common.entities.SysUser;
import com.wuxi.project.common.feign.IFeignSystemController;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IFeignSystemController feignSystemController;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 判断用户名是否为空
if (StringUtils.isEmpty(username)) {
throw new BadCredentialsException("用户名不能为空");
}
// 2. 通过用户名查询数据库中的用户信息
SysUser sysUser = feignSystemController.findUserByUsername(username);
if (sysUser == null) {
throw new BadCredentialsException("用户名或密码错误");
}
// 3. 通过用户id去查询数据库的拥有的权限信息
List<SysMenu> menuList = feignSystemController.findMenuListByUserId(sysUser.getId());
// 4. 封装权限信息(权限标识符code)
List<GrantedAuthority> authorities = null;
if (CollectionUtils.isNotEmpty(menuList)) {
authorities = new ArrayList<>();
for (SysMenu menu : menuList) {
// 权限标识
String code = menu.getCode();
authorities.add(new SimpleGrantedAuthority(code));
}
}
// 5. 构建UserDetails接口的实现类JwtUser对象
JwtUser jwtUser = new JwtUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(),
sysUser.getNickName(), sysUser.getImageUrl(), sysUser.getMobile(), sysUser.getEmail(),
sysUser.getIsAccountNonExpired(), sysUser.getIsAccountNonLocked(),
sysUser.getIsCredentialsNonExpired(), sysUser.getIsEnabled(),
authorities);
return jwtUser;
}
}
6、UserDetails
package com.wuxi.project.auth.entities;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
@Data
public class JwtUser implements UserDetails {
/**
* 用户ID
*/
private String uid;
/**
* 用户名
*/
private String username;
/**
* 密码,加密存储, admin/1234
*/
@JSONField(serialize = false) // 忽略转json
private String password;
/**
* 昵称
*/
private String nickName;
/**
* 头像url
*/
private String imageUrl;
/**
* 注册手机号
*/
private String mobile;
/**
* 注册邮箱
*/
private String email;
/**
* 帐户是否过期(1 未过期,0已过期)
* 1 true 0 false
*/
@JSONField(serialize = false) // 忽略转json
private boolean isAccountNonExpired; // 不要写小写 boolean
/**
* 帐户是否被锁定(1 未过期,0已过期)
*/
@JSONField(serialize = false) // 忽略转json
private boolean isAccountNonLocked;
/**
* 密码是否过期(1 未过期,0已过期)
*/
@JSONField(serialize = false) // 忽略转json
private boolean isCredentialsNonExpired;
/**
* 帐户是否可用(1 可用,0 删除用户)
*/
@JSONField(serialize = false) // 忽略转json
private boolean isEnabled;
/**
* 封装用户拥有的菜单权限标识
*/
@JSONField(serialize = false) // 忽略转json
private List<GrantedAuthority> authorities;
// isAccountNonExpired 是 Integer 类型接收,然后转 boolean
public JwtUser(String uid, String username, String password,
String nickName, String imageUrl, String mobile, String email,
Integer isAccountNonExpired, Integer isAccountNonLocked,
Integer isCredentialsNonExpired, Integer isEnabled,
List<GrantedAuthority> authorities) {
this.uid = uid;
this.username = username;
this.password = password;
this.nickName = nickName;
this.imageUrl = imageUrl;
this.mobile = mobile;
this.email = email;
this.isAccountNonExpired = isAccountNonExpired == 1 ? true : false;
this.isAccountNonLocked = isAccountNonLocked == 1 ? true : false;
this.isCredentialsNonExpired = isCredentialsNonExpired == 1 ? true : false;
this.isEnabled = isEnabled == 1 ? true : false;
this.authorities = authorities;
}
}
三、资源服务器
1、Token配置
package com.wuxi.project.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* @Auther: ll
*/
@Configuration
public class TokenConfig {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
}
2、资源服务器配置
package com.wuxi.project.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.TokenStore;
/**
* 资源服务器相关配置类
*
* @Auther: ll
*/
@Configuration
@EnableResourceServer // 标识为资源服务器,请求服务中的资源,就要带着token过来,找不到token或token是无效访问不了资源
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "system";
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 当前资源服务器的资源id,认证服务会认证客户端有没有访问这个资源id的权限,有则可以访问当前服务
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
;
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement() // 采用token进行管理身份,而没有采用session,所以不需要创建HttpSession
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // 请求的授权配置
// 放行以 /api 开头的请求接口
.antMatchers("/api/**").permitAll()
// 所有请求都要有all范围权限
.antMatchers("/**").access("#oauth2.hasScope('all')")
// 其他请求都要通过身份认证
.anyRequest().authenticated();
}
}