以前项目在授权的时候,都是使用的自定义的授权令牌token,具体的实现思路是在登录的时候生成一个随机的token,设置超时时间后放入redis缓存中,前端每次访问需要鉴权的资源的时候都需要附带上token令牌,后端写一个切面对访问路径进行验证,如果需要鉴权的就进行token令牌对比,通过后刷新令牌的超时时间,当前端token失效了,则返回给前端失效结果让前端重新登录。
后来又使用过shiro做过鉴权,shiro鉴权比自定义的功能强大且提供了一些操作权限及前端标签验证,使用起来更加方便,但是最近项目开发过程中,发现shiro已经无法满足项目需求了。(ps:原先项目分了两个端,一个是后台管理端接口及app接口端,两端使用的分别是shiro鉴权和自定义网关鉴权,需要将两个项目合成一个项目,因为是两个登录方式,且项目要求要使用spring security + oauth 2鉴权方式;吐槽点:明明一个自定义的鉴权完全可以实现的,可是技术总监就是要求要这样子做,作为开发的我们也是没有办法呀!谁让我们没人家级别高呢!汗~~~)
虽然使用自定义的授权方式也可以实现我们的需求,但是本着能使用开源的并且更加安全高效的原则来说,研究了一下最常见的授权方式 oauth2,并结合查看了一下spring security相结合的使用方式。
如果有想要学习的同学,可以去GitHub的这个网址查看,写的很详细:https://github.com/lexburner/oauth2-demo
好了,废话不多说,直接贴代码:
鉴权授权服务器,项目是spring boot 2.0以上版本,如果是其它的版本的话,查看上面的网址进行配置。
package com.jishi.api.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/**
* description:auth2鉴权授权服务器
*
* @author sp
* @date 2018/7/19
*/
@Configuration
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
private static final String RESOURCE_API_ID = "api";
/**
* auth 2 资源服务器
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_API_ID).stateless(true);
// resources.resourceId(RESOURCE_WEB_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
//设置不需要鉴权的目录
.authorizeRequests()
//swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/swagger-resources").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
//回调
.antMatchers("/callback/**").permitAll()
//cms
.antMatchers("/sys/user/reg").permitAll()
.antMatchers("/sys/user/sendMailCode").permitAll()
.antMatchers("/sys/user/checkMailCode").permitAll()
.antMatchers("/sys/user/forgetPassword").permitAll()
.antMatchers("/sys/user/login").permitAll()
//api
.antMatchers("/user/login/getVertifyCode/**").permitAll()//验证码接口
.antMatchers("/user/login/loginOrRegister/**").permitAll()//登录接口
.antMatchers("/creidt/protocol/**").permitAll()
.antMatchers("/user/banner/**").permitAll()//登录页合同信息
//除了以上资源,其它资源都需要鉴权才能访问
.antMatchers("/**").authenticated();
}
}
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends
AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private UserDetailsService myUserDetailService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// password 方案一:明文存储,用于测试,不能用于生产
String finalSecret = "123456";
// password 方案二:用 BCrypt 对密码编码
// String finalSecret = new BCryptPasswordEncoder().encode("123456");
// password 方案三:支持多种编码,通过密码的前缀区分编码方式
// String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode("123456");
//配置两个客户端,一个用于password认证一个用于client认证
clients.inMemory().withClient("client_1")
.resourceIds(RESOURCE_API_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.userDetailsService(myUserDetailService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//允许表单认证
oauthServer.allowFormAuthenticationForClients();
}
}
}
spring security的资源服务器
package com.jishi.api.config;
import com.google.common.collect.ImmutableList;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* description:
*
* @author sp
* @date 2018/7/19
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailService;
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder());
// }
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return myUserDetailService;
}
@Override
protected UserDetailsService userDetailsService() {
return myUserDetailService;
}
/**
* springboot2.0 删除了原来的 plainTextPasswordEncoder https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/#10.3.2
* DelegatingPasswordEncoder
*/
// password 方案一:明文存储,用于测试,不能用于生产
@Bean
@SuppressWarnings("all")
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
// password 方案二:用 BCrypt 对密码编码
// @Bean
// PasswordEncoder passwordEncoder(){
// return new BCryptPasswordEncoder();
// }
// password 方案三:支持多种编码,通过密码的前缀区分编码方式,推荐
// @Bean
// PasswordEncoder passwordEncoder(){
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
// }
/**
* 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.csrf().disable()
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers( "/api/open/**", "/","/favicon.ico").permitAll();
// @formatter:on
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(ImmutableList.of("*"));
configuration.setAllowedMethods(ImmutableList.of("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
// setAllowCredentials(true) is important, otherwise:
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
configuration.setAllowCredentials(true);
// setAllowedHeaders is important! Without it, OPTIONS preflight request
// will fail with 403 Invalid CORS request
configuration.setAllowedHeaders(ImmutableList.of("*"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
自定义的userDetailService
package com.jishi.api.service.auth;
import com.jishi.api.config.User;
import com.jishi.api.mapper.CmsSysUserMapper;
import com.jishi.commons.constants.RedisKey;
import com.jishi.commons.entity.CmsSysUser;
import com.jishi.commons.utils.DesUtil;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
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.Service;
import redis.clients.jedis.JedisCluster;
/**
* description:auth鉴权service
*
* @author sp
* @date 2018/7/19
*/
@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {
@Resource
private CmsSysUserMapper cmsSysUserMapper;
@Autowired
private JedisCluster jedisCluster;
@Override
public UserDetails loadUserByUsername(String mobileOrUsername) throws UsernameNotFoundException {
try {
//cms
if(StringUtils.equals(mobileOrUsername.split("_")[0],"cms")){
CmsSysUser cmsSysUser = new CmsSysUser();
cmsSysUser.setUsername(mobileOrUsername.split("_")[1]);
cmsSysUser.setEmail(mobileOrUsername.split("_")[1]);
CmsSysUser query = cmsSysUserMapper.getUserByName(cmsSysUser);
if(query != null){
//有数据,返回校验
User user = new User();
user.setUsername(mobileOrUsername);
user.setPassword(DesUtil.decryptFinalKey(query.getPassword()));
return user;
}else {
throw new UsernameNotFoundException("can't find user by username" + mobileOrUsername);
}
}
//app
String redisCode = jedisCluster.get(RedisKey.VERTIFYCODE.toString() + mobileOrUsername.split("_")[1]);
User user = new User();
user.setPassword(redisCode);
user.setUsername(mobileOrUsername);
return user;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
ps:自定义的userDetailService里面并没有任何比较,此处说明一下,比较是框架自行进行的,这里需要做的是返回正确的对比数据,也就是username跟password,因为我的项目是原先的两个项目结合的,一个是后台管理cmis的密码登录,另外一个是app验证码方式登录,所以这里app端的密码设置的是验证码作为密码。
具体的使用流程为:
1:cmis端,访问无需鉴权的登录接口进行登录,当登录通过后,访问http://localhost:10100/oauth/token?username=cms_admin&password=123456&grant_type=password&scope=select&client_id=client_1&client_secret=123456
获取access_token,后面需要鉴权的接口都附带上access_token即可。
2 : app端,访问app的验证码登录,通过后,访问http://localhost:10100/oauth/token?username=api_admin&password=123456&grant_type=password&scope=select&client_id=client_1&client_secret=123456
获取access_token。
注:cmis端的地址中的password是登录密码,app端的password是手机获取的验证码,框架使用的是redis作为缓存,使用的默认的端口,所以配置的时候本地需要启动一个默认配置的redis,否则会报错,后续redis的配置可以自己配置一下,或者等我后续更新。