首先分析一下为什么要用OAuth2和JWT来做
在上面流程图中第一步之后,会重定向到类似于
http://localhost:8080/token/oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token
会返回一个code
在访问
http://localhost:8080/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=
附加上code值,这时就会返回access_token
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据库保存授权用户
CREATE TABLE `sys_user` (
`id` varchar(150) NOT NULL,
`phone` varchar(50) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`password` varchar(150) NOT NULL,
`disable` int(11) NOT NULL,
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`ip` varchar(150) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
详情见如后代码
pom.xml文件
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
主要讲解这三个类,他们的的执行顺序:SecurityConfiguration->MyAuthenticationSuccessHandler->AuthorizationServerConfiguration
首先在用户中心登陆操作,获取账号密码,发送HTTP请求
private JSONObject requestToken(String account, String password, String deviceType) {
String result = null;
try {
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategy()).build();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
MultiValueMap sendMap = new LinkedMultiValueMap<>();
sendMap.add("username", account);
sendMap.add("password", password);
result = RestTemplateUtil.postForEntityFormData(restTemplate, Datas.AUTH_LOGIN_URL, sendMap, deviceType);
logger.info("认证中心返回结果-------》》》》》" + result);
} catch (Exception e) {
logger.error("error", e);
throw new Exception("500", e);
}
return JSON.parseObject(result);
}
方法执行顺序:userDetailsService(会获取到传进来的username)->protected void configure(HttpSecurity http)
->这里会执行config里的myAuthenticationFailureHandler->(这是第二个类)
import javax.transaction.Transactional;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dao.UserMapper;
import org.bifu.distributed.auth.domain.User;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.method.configuration.EnableGlobalMethodSecurity;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Autowired
private UserMapper userMapper;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户来源于数据库
// auth.userDetailsService(userDetailsService()).passwordEncoder(new MyPasswordEncoder());
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
// auth.userDetailsService(userDetailsService()).passwordEncoder(MyPasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
* formLogin()对应表单认证相关的配置
* logout()对应了注销相关的配置
* httpBasic()可以配置basic登录
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginProcessingUrl("/auth/login").successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler).and().csrf().disable().sessionManagement()
.maximumSessions(1).expiredUrl("/expiredSession");
logger.info("test是否生成token");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
@Transactional(rollbackOn = Exception.class)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//(注:用户名的name属性必须为username,密码的name属性必须为password。这是security判断用户输入是否正确的标准)
//
logger.info("登录手机号或邮箱:======"+username);
// 查用户
User user = userMapper.selectByPhoneOrEmail(username, username);
if (user == null) {
throw new UsernameNotFoundException(AuthContants.USER_NOT_EXIST);
}
SecurityUserDTO dto = new SecurityUserDTO();
dto.setId(user.getId());
dto.setUsername(username);
dto.setPassword(user.getPassword());
dto.setDisable(user.getDisable());
// 创建securityUserDTO
// SecurityUserDTO securityUserDTO = new SecurityUserDTO(user);
return dto;
}
};
}
}
这是第二个类,这里会进行重定向获取code
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.bifu.distributed.auth.constant.AuthContants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final static Logger logger = LoggerFactory.getLogger(MyAuthenticationSuccessHandler.class);
@Value(value = "${prefix.auth}")
private String authPrefix; // /token
@Value(value = "${oauth.redirectUrl}")
private String redirectUrl; // /token
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String deviceType = request.getHeader("deviceType");
logger.info("访问设备-----------》》》" + deviceType);
if (deviceType == null || "".equals(deviceType)) {
deviceType = "browser";
}
// 重定向url到 /token 接口
if ("browser".equals(deviceType)) {
response.sendRedirect("http://localhost:8080:oauth/authorize?client_id=client1&response_type=code&redirect_uri=/token");
} else if ("app".equals(deviceType)) {
response.sendRedirect(http://localhost:8080:oauth/authorize?client_id=client2&response_type=code&redirect_uri=/token);
}
}
}
因为重定向的url是:redirect_uri=/token
生成一个jks
keytool -genkeypair -alias kevin_key -keyalg RSA -keypass 123456 -keystore kevin_key.jks -storepass 123456
导出公钥
keytool -list -rfc --keystore kevin_key.jks | openssl x509 -inform pem -pubkey
保存文本public_key.txt
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxeI6+R6DsGs5RW21Xu1Fur7iPwGjyngN3SCnwPtdR9jTrQ8EIak+gyjpI/g7gIacHIZKMlVFWoEgjQ7+hIQ5FHBrmSR/S81ezCFjYSjBbdrHYQjMRpn4mEWFmQhIyTRhg1Pb5oTUlWx+L3wc45r6JFdMOlgkKBvfo/7lzwGhxeNp10rfoJcnGDhlfZ3PmoIOYmvg7Z8UwszZpYHWf98164m3hMiPyc81iiy/DEE60OVVepyvynfBwg1aGDyA64w63FZ/2dSwfQ/7VQ7WWJb7oVoIy5pyHslWMuQJPpNCxpOgmb19AgC1GojDSL7WAEq+2gQFrb+7k4PyBdsRYzR9DQIDAQAB
-----END PUBLIC KEY-----
认证服务端在Resource保存jks,认证资源服务端保存public_key.txt
下面类主要执行的方法是:accessTokenConverter
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import com.alibaba.fastjson.JSONObject;
import org.bifu.distributed.auth.constant.AuthContants;
import org.bifu.distributed.auth.dto.SecurityUserDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
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.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
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.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
/**
* 认证授权服务端
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class);
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//开启密码授权类型
endpoints.authenticationManager(this.authenticationManager);
endpoints.accessTokenConverter(accessTokenConverter());
//配置token存储方式
endpoints.tokenStore(tokenStore());
endpoints.reuseRefreshTokens(false);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// jdbc方式
clients.withClientDetails(clientDetails());
}
/**
* token converter
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
/***
* 重写增强token方法,用于自定义一些token返回的信息
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
SecurityUserDTO securityUserDTO =
(SecurityUserDTO) authentication.getUserAuthentication().getPrincipal();
logger.info("重写增强token方法= {}", JSONObject.toJSONString(securityUserDTO));
final Map additionalInformation = new HashMap<>(16);
additionalInformation.put("userId", securityUserDTO.getId());
((DefaultOAuth2AccessToken) accessToken)
.setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
// 非对称加密
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("kevin_key.jks"),
"123456".toCharArray());
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("kevin_key"));
return accessTokenConverter;
}
/**
* 定义clientDetails存储的方式-》Jdbc的方式,注入DataSource
*
* @return
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* token store
*
* @param
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
}
现在重定向到/token这个接口
/**
* 授权,登录
*/
@ResponseBody
@RequestMapping(value = "/token")
public ResultDTO token(HttpServletRequest request, HttpServletResponse response,
RedirectAttributes attributes) {
TokenResultDTO result = this.userService.token(attributes, request, response);
logger.info("获取到token= {}", JSONObject.toJSONString(result));
return new ResultDTO("200", "succ", result);
}
//DTO类
@Data
public class TokenResultDTO {
private String access_token;
private String token_type;
private String expires_in;
private String scope;
private String jti;
private String refresh_token;
private String userId;
}
#####访问的url路径 是固定的
public TokenResultDTO token(RedirectAttributes attributes, HttpServletRequest request,
HttpServletResponse response) {
try {
String code = request.getParameter("code");
if (StringUtils.isEmpty(code)) {
throw new BusinessException(AuthContants.CODE_EXCEPTION);
}
// 发送请求token
String deviceType = "browser";
if (request.getHeader("deviceType") != null && !"".equals(request.getHeader("deviceType"))) {
deviceType = request.getHeader("deviceType");
}
HttpHeaders headers = new HttpHeaders();
HttpEntity entity = new HttpEntity(headers);
TokenResultDTO tokenResultDTO = null;
if ("browser".equals(deviceType)) {
tokenResultDTO = this.browserRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client1&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
} else if ("app".equals(deviceType)) {
tokenResultDTO = this.appRestTemplate.postForObject(
" http://localhost/oauth/token?client_id=client2&grant_type=authorization_code&redirect_uri=/token&code=" + code,
entity, TokenResultDTO.class);
}
return new TokenResultDTO(tokenResultDTO.getAccess_token(), tokenResultDTO.getRefresh_token(),
tokenResultDTO.getUserId(), tokenResultDTO.getExpires_in());
} catch (BusinessException e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
} catch (Exception e) {
logger.error("token?");
throw new Exception("500", e.getMessage());
}
}
到这里就获取到了access_token
log日志:
这里只讲解ResourceServerConfiguration这个类,其他都是检查异常的
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
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.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
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.JwtTokenStore;
import org.springframework.security.web.AuthenticationEntryPoint;
/**
* 认证授权资源端
*
* @author rs
*
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private Auth2ResponseExceptionTranslator auth2ResponseExceptionTranslator;
@Autowired
private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("client1");
resources.tokenServices(defaultTokenServices());
// 定义异常转换类生效
AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
((OAuth2AuthenticationEntryPoint) authenticationEntryPoint)
.setExceptionTranslator(this.auth2ResponseExceptionTranslator);
resources.authenticationEntryPoint(authenticationEntryPoint);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 放行路径在这写
http.csrf().disable().exceptionHandling().authenticationEntryPoint(this.securityAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler).and().authorizeRequests()
.antMatchers("/swagger-resources/**", "/v2/**", "/swagger/**", "/swagger**", "/webjars/**", "/aide/**",
"/backstage/**", "/coin/**", "/talla/**", "/asset/**", "/test/**","/blockchain/borrow/**", "/back/**")
.permitAll().anyRequest().authenticated().and().httpBasic().disable();
// ifream的跨域设置
http.headers().frameOptions().sameOrigin();
}
// ===================================================以下代码与认证服务器一致=========================================
/**
* token存储,这里使用jwt方式存储
*
* @param
* @return
*/
@Bean
public TokenStore tokenStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
/**
* 创建一个默认的资源服务token
*
* @return
*/
@Bean
public ResourceServerTokenServices defaultTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
/**
* Token转换器必须与认证服务一致
*
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
};
Resource resource = new ClassPathResource("public_key.txt");
String publicKey = null;
try {
publicKey = inputStream2String(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
accessTokenConverter.setVerifierKey(publicKey);
return accessTokenConverter;
}
// ===================================================以上代码与认证服务器一致=========================================
private String inputStream2String(InputStream is) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(is));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = in.readLine()) != null) {
buffer.append(line);
}
return buffer.toString();
}
}
主要讲解public void configure(HttpSecurity http)这个方法:我们在测试的时候往往希望直接就能访问到,不需要token,可以在这里添加放行路径。
个人理解,欢迎指正交流哦!!!,随后会把代码公布在gitee中:https://gitee.com/ran_song
欢迎关注我的微信公众号<搜索:汀雨笔记>,会首发一些最新文章哦!
下面是我的个人网站:http://ransongv587.com