1.背景
项目由springboot1.5.X升级到springboot2.0.0后,导致各组件API以及依赖包发生了变化。
完整项目demo:码云
2.spring security
Spring Security 从入门到进阶系列教程网址:http://www.spring4all.com/article/428
- spring security架构图
- 认证过程
3.OAuth2
- OAuth2基础概念网址:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
- OAuth2分为3个部分:1认证服务器2资源服务器3第三方应用
- OAuth2有4种授权模式:1授权码模式2简化模式3密码模式4客户端模式
4.使用springboot2+oauth2注意事项
- 项目搭建参考网址:
https://blog.csdn.net/qq_19671173/article/details/79748422
http://wiselyman.iteye.com/blog/2411813
4.1.在pom.xml文件中导入依赖包发生变化
<!-- springboot2.0已经将oauth2.0与security整合在一起 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 由于一些注解和API从spring security5.0中移除,所以需要导入下面的依赖包 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<!-- redis相关依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
4.2.直接使用RedisTokenStore存储token会出现NoSuchMethodError RedisConnection.set([B[B)V错误
解决方案:自己编写一个MyRedisTokenStore,复制RedisTokenStore类中代码,并将代码中conn.set(accessKey, serializedAccessToken)修改为conn.stringCommands().set(accessKey, serializedAccessToken);
4.3.前后端分离时,存在跨域问题
解决方案:
- 方案一在后端注册corsFilter
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 1
corsConfiguration.addAllowedOrigin("*");
// 2
corsConfiguration.addAllowedHeader("*");
// 3
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// step 4
source.registerCorsConfiguration("/**", buildConfig());
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
- 方案二,在启动类添加bean到IOC容器中
@SpringBootApplication
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
/**
* 解决前后端分离跨域问题
*
* @return
*/
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
4.4.前后端分离,登录页面放在前端时登录的问题
解决方案:授权模式使用password的方式
- 使用post请求访问http://localhost:20000/auth/oauth/token
- 在请求的headers中新增一个header:key=Authorization,value=Basic Y2xpZW50OnNlY3JldA==
(Y2xpZW50OnNlY3JldA==为64编码,格式:client:secret)
- 在form-data中传递参数:username(用户账号)、password(用户密码)、grant_type(固定值:password)、scope(作用域)
4.5.访问资源服务器的方式
- 当通过在认证服务器获取到token后,有三种方式访问资源服务器
- 在Headers中携带:key=Authorization,value=bearer 797c4200-8b10-4a2b-8764-33397749a8f7
- 拼接在URL中:http://localhost:8088/user?access_token=797c4200-8b10-4a2b-8764-33397749a8f7
- 在form表单中:name=access_token,value=797c4200-8b10-4a2b-8764-33397749a8f7
4.6.spring security密码配置问题
- secret密码配置从 Spring Security 5.0开始必须以 {加密方式}+加密后的密码 这种格式填写
- 当前版本5新增支持加密方式:
bcrypt - BCryptPasswordEncoder (Also used for encoding)
ldap - LdapShaPasswordEncoder
MD4 - Md4PasswordEncoder
MD5 - new MessageDigestPasswordEncoder(“MD5”)
noop - NoOpPasswordEncoder
pbkdf2 - Pbkdf2PasswordEncoder
scrypt - SCryptPasswordEncoder
SHA-1 - new MessageDigestPasswordEncoder(“SHA-1”)
SHA-256 - new MessageDigestPasswordEncoder(“SHA-256”)
sha256 - StandardPasswordEncoder
4.7.通过spring security的角色限制访问受保护的接口
- 在配置类或启动类上添加注解 @EnableGlobalMethodSecurity(securedEnabled = true)
@EnableOAuth2Sso
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/index")
.permitAll()
.anyRequest()
.authenticated();
}
}
- 在controller的类或方法上添加注解 @Secured(“ROLE_ADMIN”)
package com.bici.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author: keluosi@bicitech.cn
* @date: 2018/4/17
*/
@RestController
@RequestMapping("/client")
@Secured("ROLE_ADMIN")
public class ClientController {
@GetMapping("/user")
public Authentication getUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
@GetMapping("/index")
@Secured("ROLE_USER")
public String index() {
return "index";
}
}
4.8.使用自定义的加密方式校验数据库中保存的加密后的密文
- 在@EnableWebSecurity注解的方法中编写代码
import com.bici.encrypt.EncryptUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.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.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Qualifier("userDetailsService")
@Autowired
private UserDetailsService userDetailsService;
@Override
protected UserDetailsService userDetailsService() {
// 自定义用户信息类
return this.userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder(){
@Override
public String encode(CharSequence charSequence) {
// 加密
return EncryptUtil.hashPasswordAddingSalt(charSequence.toString());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
// 密码校验
return EncryptUtil.isValidPassword(charSequence.toString(), s);
}
}) ;
}
}
4.9.通过配置返回通知获取token
可以在这个地方将token和username存入到缓存中,然后如果需要强制某个用户下线时,通过username从缓存中找到token,调用ConsumerTokenServices
的revokeToken(token)
方法。
package com.zkane.aspect;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
/**
* @author: 594781919@qq.com
* @review:
* @date: 2018/8/24
*/
@Aspect
@Component
public class TokenAspect {
@Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
private void token() {
}
@AfterReturning(returning = "obj", pointcut = "token()")
public void doAfterReturning(ResponseEntity<OAuth2AccessToken> obj) throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 打印用户名
System.out.println(request.getParameter("username"));
// 打印token
System.out.println(obj.getBody().getValue());
}
}
5.不足或后续改进
5.1.客户端信息保存到数据中
- 创建sql语句
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO oauth_client_details (
client_id,
resource_ids,
client_secret,
scope,
authorized_grant_types,
web_server_redirect_uri,
authorities,
access_token_validity,
refresh_token_validity,
additional_information,
autoapprove
)
VALUES
(
'client',
NULL,
'{noop}secret',
'all',
'password,authorization_code,refresh_token,implicit,client_credentials',
NULL,
NULL,
NULL,
NULL,
NULL,
'true'
);
- 以jdbc方式配置客户端信息
@Configuration
@EnableAuthorizationServer
public class ServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
}
5.2.前后端分离,导致单点登录问题
- 目前通过网上下载的demo,还没有查找到第三方应用前后端分离,怎么去登录的问题。
- 第三方应用一般都是通过请求转发页面时,到认证中心去登录。也就是前后端未分离的情况,使用注解@EnableOAuth2Sso
- 建议解决方案:通过前端进行跳转到唯一的登录页面,登录成功后再返回到原来系统并带上token
5.3.密码错误时,返回前端的json未实现自定义返回内容
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
update 2018-04-28
自定义返回前端的登录错误信息
- 将spring-security-core\5.0.3.RELEASE\org\springframework\security\messages_zh_CN.properties拷贝到resources目录下
- 在启动类中编写代码
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
return messageSource;
}
}
- 显示效果,error_description是在messages_zh_CN.properties中自己根据需要编写的
{
"error": "invalid_grant",
"error_description": "账号未注册,请联系管理员"
}