一、参数配置
1)配置参数:application.yml
springcloud:
security:
oauth2:
clients[0]:
clientId: zuul-server
clientSecret: '12345678'
accessTokenValiditySeconds: 7200 //通行token有效时间(秒)
refreshTokenValiditySeconds: 604800 //刷新token有效时间(秒)
clients[1]:
clientId: user-server
clientSecret: '123456'
accessTokenValiditySeconds: 7200
refreshTokenValiditySeconds: 604800
2)单个客户端配置封装类:OAuth2ClientProperties.java
@Getter @Setter public class OAuth2ClientProperties {
private String clientId; //客户端id
private String clientSecret; //客户端秘钥
private Integer accessTokenValiditySeconds; //通行token有效时长
private Integer refreshTokenValiditySeconds; //刷新token有效时长
}
3)从配置文件获取配置封装到配置集合中:OAuth2Properties.java
@ConfigurationProperties(prefix = "springcloud.security.oauth2") //匹配前缀为springcloud.security.oauth2的参数
public class OAuth2Properties
{
private OAuth2ClientProperties[] clients = {};//把参数放到数组中,此处也可以从数据库获取,本例子直接配置获取
public OAuth2ClientProperties[] getClients() {
return clients;
}
//OAuth2ClientProperties 单个配置实体封装
public void setClients(OAuth2ClientProperties[] clients) {
this.clients = clients;
}
}
4)核心配置--扫描、注册自定义配置参数
@Configuration
@EnableConfigurationProperties(OAuth2Properties.class)
public class OAuth2CoreConfig
{
}
二、业务配置
1)认证授权参数配置
@Configuration
@EnableResourceServer
@EnableAuthorizationServer //开启授权服务
public class OAuth2Config1 extends AuthorizationServerConfigurerAdapter
{
private JsonParser objectMapper = JsonParserFactory.create();
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private OAuth2Properties oauth2Properties;//注入配置类
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//这句不知道是干啥的,大概是开启一个身份验证表单的功能吧,就是弹出一个输入账户、密码的框
oauthServer.allowFormAuthenticationForClients();
}
/*配置客户端基本信息,循环配置数组,加载到配置服务运行内存,用于授权判断*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 客户端配置化
InMemoryClientDetailsServiceBuilder build = clients.inMemory();
if (ArrayUtils.isNotEmpty(oauth2Properties.getClients())) {
// 验证模式:刷新token,密码、验证码模式,也可根据客户端自定义配置
for (OAuth2ClientProperties config : oauth2Properties.getClients()) {
build.withClient(config.getClientId()).secret(config.getClientSecret())
.accessTokenValiditySeconds(config.getAccessTokenValiditySeconds()) //通行token
.refreshTokenValiditySeconds(config.getRefreshTokenValiditySeconds()) // 刷新token
.authorizedGrantTypes("refresh_token", "password", "authorization_code")//验证模式
.scopes("platform");//作用域,可以在配置中按实际业务自定义各个客户端的作用域,写死并不可取
}
}
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//JWT方式
endpoints.tokenStore(tokenStore())//设置token存储方式,见方法实现
.tokenEnhancer(jwtTokenEnhancer()) //设置token的加密方式,见方法实现
.authenticationManager(authenticationManager);//开启密码类型验证的bean
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
/*JWT token生成规则,此处对原来的转换器 JwtAccessTokenConverter 下的方法 enhance() 进行重写,
来补充生成token中自定义的业务字段,如组织架构id、用户id等字段*/
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
/*重写token生成方法,补充组织架构、用户id内容*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
Map info = new LinkedHashMap(accessToken.getAdditionalInformation());//附加信息map
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
//自定义信息设置到token负载中
SysUser user = (SysUser)authentication.getPrincipal();//SysUser:系统的登录用户实体,根据实际业务设计
info.put("orgId", user.getOrgId());//操作用户所在的组织架构id
info.put("userId", user.getId());//操作用户的id
try {
//用户名,因为是中文,为避免乱码,在设置时进行了编码转换,此处先解码
info.put("uname", URLEncoder.encode(user.getName(), "GBK"));
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
info.put("loginName", user.getLoginName());//当前登录名
info.put("dataAccess", user.getDataAccess());//当前用户的数据权限(数组)
result.setAdditionalInformation(info);//把解析好的附加信息设置到结果集中
result.setValue(encode(result, authentication));//对设置好的结果集编码
OAuth2RefreshToken refreshToken = result.getRefreshToken();//获取结果中的刷新token
if (refreshToken != null) {
//下面这一段,我也看不大懂,不知道从哪拼凑来的,勉强能用吧[捂脸],大概意思就是,生成token,与附加信息合并返回
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
encodedRefreshToken.setExpiration(null);
try {
Map claims = objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
Map refreshTokenInfo = new LinkedHashMap(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);//
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken(token);
}
return result;
}
};
//根据私钥加锁,客户端需要拿匹配的公钥进行解锁,保证安全性,私钥使用Java Keytool 工具生成
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("fzp-jwt.jks"),
"123456789".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));
return converter;
}
}
2)基于web的security基本配置
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)//方法级别的安全支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
UserService userService;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 代码解析:
* 1.在内存中创建一个认证用户信息
* 2.该认证名为forezp,密码为123456,有USER的角色
* 防护工作:
* 1.应用的每一个请求都需要验证
* 2.自动生成一个登录表单
* 3.可以用username和password来进行验证
* 4.可以注销
* 5.阻止CSRF攻击
* 6.Session Fixation保护
* 7.安全Header集成......
* 8.Servlet API方法集成:
* HttpServletRequest#getRemoteUser()
* HttpServletRequest.html#getUserPrincipal()
* HttpServletRequest.html#getUserInRole(String)
* HttpServletRequest.html#login(String,String)
* HttpServletRequest.html#logout()
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//集成JWT
http.csrf().disable().exceptionHandling().accessDeniedPage("/401")
.and()
.authorizeRequests().anyRequest().fullyAuthenticated()
.and()
.httpBasic()
.and().rememberMe().and()
.formLogin().loginPage("/login").failureUrl("/login?error").permitAll() //登录页面用户任意访问
.and()
.logout().permitAll(); //注销行为任意访问
}
/**
* 用户信息服务userService,具体实现见实体
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//验证使用自定义的userService
auth.userDetailsService(userService);//.passwordEncoder(new BCryptPasswordEncoder());
}
}
3)userService用户服务类实现
@Service
public class UserService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
@Autowired
UserDao userDao;//用户dao
@Autowired
RoleDao roleDao;//角色dao
@Autowired
PermissionDao permissionDao;//权限dao
/**
* 根据登录名获取用户实体信息
*/
@Override
public SysUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
try {
SysUser user = new SysUser();
List grantedAuthorities = new ArrayList();
SysUser query = new SysUser();
query.setLoginName(loginName);
user = (SysUser) this.userDao.templateOne(query);
if (user != null) {
Map paramMap = new HashMap();
paramMap.put("userId", user.getId());
List permissions = permissionDao.findByAdminUserId(paramMap);//接口调用权限
List
以上,授权中心的配置核心部分基本完成
三、资源配置(在网关完成,由网关统一拦截请求,并对请求进行身份验证)
1)先附上两个异常处理类
//token验证异常封装类
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws ServletException {
Map map = new HashMap();
Throwable cause = authException.getCause();
if(cause instanceof InvalidTokenException) {
map.put("code", RespCode.INVALID_TOKEN);//402
map.put("msg", "无效的token");
}else{
map.put("code", RespCode.UN_LOGIN);//401
map.put("msg", "访问此资源需要完全的身份验证");
}
map.put("data", authException.getMessage());
map.put("success", false);
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(System.currentTimeMillis()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), map);
} catch (Exception e) {
throw new ServletException();
}
}
}
//权限不足异常封装类
@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map map = new HashMap();
map.put("code", RespCode.UNAUTHORIZED);//401
map.put("msg", "权限不足");
map.put("data", accessDeniedException.getMessage());
map.put("success", false);
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(System.currentTimeMillis()));
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(mapper.writeValueAsString(map));
}
}
2)JWT解密配置类
@Configuration
public class JwtConfig {
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
*
* @Title: jwtAccessTokenConverter
* @Description: JWT解密
* @return
* JwtAccessTokenConverter
*/
@Bean
protected JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");//拿到配置的公钥文件
String publicKey;
try {
//生成公钥
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (Exception e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);//用公钥解密
return converter;
}
}
3)资源服务配置类
@Configuration
@EnableResourceServer
@RefreshScope
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
TokenStore tokenStore;
@Autowired
private PermitConfig permitConfig;//此处配置那些不需要权限验证的服务接口
@Autowired
MyFilterSecurityInterceptor myFilterSecurityInterceptor;//自定义的方法放行判定类
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authenticated = http.csrf().disable().authorizeRequests()
.antMatchers("/config/**").permitAll();
String[] permitPaths = permitConfig.getPermitPaths();
if(permitPaths != null){
authenticated.antMatchers(permitPaths).permitAll();
}
authenticated.antMatchers("/**").authenticated();
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);//添加自定义方法过滤器
}
@Override
public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
//增加自定义的错误异常接收类,封装错误信息
resource.tokenStore(tokenStore).authenticationEntryPoint(new AuthExceptionEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler());
}
}
4)自定义方法过滤器,定义方法通行的规则
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter
{
@Autowired
private MyInvocationSecurityMetadataSourceService securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
/**
* 初始化当前登录人、组织架构参数
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// fi里面有一个被拦截的url
// 里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
// 再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
5)自定义权限加载--数据库权限配置获取,打破代码注解接口限制,可为每一个url配置访问权限
@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource,InitializingBean{
@Autowired
private RedisApi redisApi;//Redis接口服务,把配置放进redis,避免频繁查询数据库
@Autowired
private PermissionDao permissionDao; //权限dao
private static ConcurrentHashMap> map = new ConcurrentHashMap>();
/**
* 加载权限:从数据权限表获取系统所有权限,解析(系统的所有权限包括列表权限、操作权限均先配置在数据库中)
* 问题:用户登录时获取token中存入权限信息,中途权限修改后,token中的权限并不会变:需要重新登陆获取新的token后才生效
*/
public void loadResourceDefine() {
List permissions = permissionDao.all();//获取所有配置权限
Collection array;
ConfigAttribute cfg;
map.clear();
//map = new HashMap<>();
for (SysPermission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
// 此处只添加了权限的请求名称,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。
//此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
// 用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
map.put(permission.getUrl(), array);
}
//Redis缓存服务中刷新标识归位(设置2分钟便于测试,修改权限时刷新该标识,可适当延长周期)
redisApi.set(ProjectConstant.PERMISSION_UPDATE_FLAG, false, 2L, TimeUnit.MINUTES);
}
// 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。
// 如果不在权限表中则放行。
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
if (getRefreshFlag()) {
//刷新标识开启时,重新加载配置列表(后台权限有修改时,该标识开启,说明权限信息有变更)
loadResourceDefine();
}
// object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if (matcher.matches(request)) { return map.get(resUrl); }
}
return null;
}
/**
*
* @Title: getRefreshFlag
* @Description: 检查权限map是否需要刷新--权限修改时,该标识生效,刷新map
* @return
* boolean
* @throws
*/
private boolean getRefreshFlag() {
try {
if (!redisApi.exists(ProjectConstant.PERMISSION_UPDATE_FLAG)) {
//缓存不存在--首次,刷新
return true;
}else {
if((Boolean) redisApi.get(ProjectConstant.PERMISSION_UPDATE_FLAG)) {
//权限被修改,刷新
return true;
}else {
return false;
}
}
} catch (Exception e) {
//其他异常:刷新
return true;
}
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
//初始化
@Override
public void afterPropertiesSet() throws Exception {
loadResourceDefine();
}
}
6)自定义权限验证类
@Service
public class MyAccessDecisionManager implements AccessDecisionManager
{
// decide 方法是判定是否拥有权限的决策方法,
// authentication 是UserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
// object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
// configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url
// 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || configAttributes.size() <= 0) {
return;
}
ConfigAttribute c;
String needRole;
for (Iterator iter = configAttributes.iterator(); iter.hasNext();) {
c = iter.next();
needRole = c.getAttribute();
// authentication 为在注释1 中循环添加到 GrantedAuthority对象中的权限信息集合
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needRole.trim().equals(ga.getAuthority())) {
return; //有权限,放行
}
}
}
throw new AccessDeniedException("access denied");//无权限返回
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
此处网关出的核心代码基本晒完了,可能有部分细节的遗漏。
以上方案为本人根据各项网站资料、书本资料及个人所感所悟所整合出来的权限管理方案,深知其中仍有许多不足,仅作个人总结及经验分享之用,请尊重个人劳动成果,勿用于商业用途,若以上内容有不足之处,欢迎广大道友指出!