一、背景
随着项目越来越大,需要将多个服务拆分成微服务,使代码看起来不要过于臃肿,庞大。微服务之间通常采取feign交互,为了保证不同微服务之间增加授权校验,需要增加Spring Security登录验证,为了多个服务之间session可以共享,可以通过数据库实现session共享,也可以采用redis-session实现共享。
本文采取Spring security做登录校验,用redis做session共享。实现单服务登录可靠性,微服务之间调用的可靠性与通用性
二、代码
本文项目采取 主服务一服务、子服务二 来举例
1、服务依赖文件
主服务依赖
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.5.4'
implementation group: 'org.springframework.session', name: 'spring-session-data-redis', version: '2.4.1'
implementation(group: 'io.github.openfeign', name: 'feign-httpclient')
implementation 'org.springframework.boot:spring-boot-starter-security'
子服务依赖
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.5.4'
implementation group: 'org.springframework.session', name: 'spring-session-data-redis', version: '2.4.1'
implementation 'org.springframework.boot:spring-boot-starter-security'
2、服务配置文件
主服务配置文件
#redis连接
spring.redis.host=1.2.3.4
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码
spring.redis.password=password
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
#数据库
spring.redis.database=0
#redis-session配置
spring.session.store-type=redis
#部分post请求过长会报错,需加配置
server.tomcat.max-http-header-size=65536
子服务配置文件
#单独登录秘钥
micService.username=service
micService.password=aaaaaaaaaaaaa
#登录白名单
micService.ipList=1.2.3.4,1.2.3.5,127.0.0.1,0:0:0:0:0:0:0:1
spring.redis.host=1.2.3.4
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码
spring.redis.password=password
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
#数据库
spring.redis.database=0
#最大请求头限制
server.maxPostSize=-1
server.maxHttpHeaderSize=102400
#redis session缓存
spring.session.store-type=redis
server.servlet.session.timeout=30m
3、登录校验文件
主服务SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入密码加密的类
@Bean
public AuthenticationProvider authenticationProvider() {
AuthenticationProvider authenticationProvider = new EncoderProvider();
return authenticationProvider;
}
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider());
}
public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successHandler((request,response,authentication) -> {
HttpSession session = request.getSession();
session.setAttribute("TaobaoUser",authentication.getPrincipal());
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
TaobaoUser user = (CurrentUser)session.getAttribute("TaobaoUser");
out.write(mapper.writeValueAsString(user));
out.flush();
out.close();
})
.failureHandler((request,response,authentication) -> {
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(mapper.writeValueAsString(new ExceptionMessage("400",authentication.getMessage())));
out.flush();
out.close();
})
.loginPage("/Login.html")
.loginProcessingUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/api/common/invalidUrl","/**/*.css", "/**/*.js", "/**/*.gif ", "/**/*.png ", "/**/*.jpg", "/webjars/**", "**/favicon.ico", "/guestAccess", "/Login.html",
"/v2/api-docs","/configuration/security","/configuration/ui","/api/common/CheckLatestVersionInfo").permitAll()
.anyRequest()
//任何请求
.authenticated()
.and()
.sessionManagement()
.maximumSessions(-1)
.sessionRegistry(sessionRegistry());
;
http.csrf().disable();
}
@Autowired
private FindByIndexNameSessionRepository sessionRepository;
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(){
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
}
EncoderProvider.java
java
@Service
public class EncoderProvider implements AuthenticationProvider {
public static final Logger logger = LoggerFactory.getLogger(EncoderProvider.class);
/**
* 自定义验证方式
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
//支持用户名和员工号登录 可能为用户名或员工号
String username = authentication.getName();
String credential = (String) authentication.getCredentials();
//加密过程在这里体现
TaobaoUser user= userService.getUserData(username);
//校验,用户名是否存在
if(user==null){
throw new DisabledException("用户名或密码错误");
}
//校验登录状态
checkPassword()
Collection<GrantedAuthority> authorities = new ArrayList<>();
return new UsernamePasswordAuthenticationToken(userCurrent, credential, authorities);
} catch (Exception ex) {
ex.printStackTrace();
throw new DisabledException("登录发生错误 : " + ex.getMessage());
}
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
子服务SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入密码加密的类
@Bean
public AuthenticationProvider authenticationProvider() {
AuthenticationProvider authenticationProvider = new EncoderProvider();
return authenticationProvider;
}
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
logger.info("用户登录日志test1 username:"+http.toString());
http.formLogin()
.loginProcessingUrl("/login")
.successHandler((request,response,authentication) -> {
HttpSession session = request.getSession();
session.setAttribute("TaobaoUser",authentication.getPrincipal());
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
TaobaoUser user = (TaobaoUser )session.getAttribute("TaobaoUser");
out.write(mapper.writeValueAsString(user));
out.flush();
out.close();
})
.failureHandler((request,response,authentication) -> {
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(mapper.writeValueAsString(new ExceptionMessage("400",authentication.getMessage())));
out.flush();
out.close();
})
.loginPage("/Login.html")
.and()
.authorizeRequests()
.antMatchers("/**/*.css", "/**/*.js", "/**/*.gif ", "/**/*.png ",
"/**/*.jpg", "/webjars/**", "**/favicon.ico", "/Login.html",
"/v2/api-docs","/configuration/security","/configuration/ui").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.maximumSessions(-1)
.sessionRegistry(sessionRegistry());
http.csrf().disable();
}
@Autowired
private FindByIndexNameSessionRepository sessionRepository;
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(){
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
}
EncoderProvider.java
@Service
public class EncoderProvider implements AuthenticationProvider {
public static final Logger logger = LoggerFactory.getLogger(EncoderProvider.class);
@Value("${service.username}")
private String userName1;
@Value("${service.ipList}")
private String ipList;
/**
* 自定义验证方式
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
//支持用户名和员工号登录 可能为用户名或员工号
String username = authentication.getName();
String credential = (String)authentication.getCredentials();
TaobaoUser user=new TaobaoUser();
if(username.equals(userName1)){
List<String> ips = Arrays.asList(ipList.split(","));
WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
String remoteIp = details.getRemoteAddress();
logger.info("ip为{}-通过用户{}调用接口",remoteIp,username);
if(!ips.contains(remoteIp)){
throw new DisabledException("无权登陆!");
}
}else{
throw new DisabledException("账户不存在!");
}
user.setA("A");
Collection<GrantedAuthority> authorities = new ArrayList<>();
return new UsernamePasswordAuthenticationToken(currentUser, credential, authorities);
} catch (Exception ex) {
ex.printStackTrace();
throw new DisabledException("登录发生错误 : " + ex.getMessage());
}
}
@Override
public boolean supports(Class<?> arg0) {
return true;
}
}
4、主服务feign配置
FeignManage.java
java
#url = "${file.client.url}",
@FeignClient(name="file-service",
fallback = FeignFileManageFallback.class,
configuration = FeignConfiguration.class)
public interface FeignFileManage {
@RequestMapping(value = "/file/upload", method = {RequestMethod.POST}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ApiBaseMessage fileUpload(@RequestPart("file") MultipartFile file, @RequestParam("fileName") String fileName) ;
}
java
public class FeignManageFallback implements FeignManage{
@Override
public ApiBaseMessage fileUpload(MultipartFile file, String type) {
return ApiBaseMessage.getOperationSucceedInstance("400","失败");
}
}
FeignFileManageFallback.java
FeignConfiguration.java
java
@Configuration
@Import(FeignClientsConfiguration.class)
public class FeignConfiguration {
/**
删除请求头文件
*/
final String[] copyHeaders = new String[]{"transfer-encoding","Content-Length"};
@Bean
public FeignFileManageFallback echoServiceFallback(){
return new FeignFileManageFallback();
}
@Bean
public FeignBasicAuthRequestInterceptor getFeignBasicAuthRequestInterceptor(){
return new FeignBasicAuthRequestInterceptor();
}
/**
* feign 调用,添加CurrentUser
*/
private class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
//删除的请求头
if (!Arrays.asList(copyHeaders).contains(name)) {
template.header(name, values);
}
}
}
}else{
template.header("Accept", "*/*");
template.header("Accept-Encoding", "gzip, deflate, br");
template.header("Content-Type", "application/json");
}
//增加用户信息
if(requestAttributes!=null && SessionHelper.getCurrentUser()!=null){
try {
template.header("TaobaoUser", URLEncoder.encode(JSON.toJSONString(SessionHelper.getCurrentUser()),"utf-8") );
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
}
}
5、主服务session文件
SessionUtil.java
java
public class SessionUtil {
public static CurrentUser getCurrentUser() {
HttpSession session = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();
CurrentUser user = (CurrentUser)session.getAttribute("TaobaoUser");
return user;
}
public static void setCurrentUser(String userName){
HttpSession session = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();
Collection<GrantedAuthority> authorities = new ArrayList<>();
session.setAttribute("TaobaoUser",new CurrentUser(userName, "", authorities));
}
}
三、完成配置后
1、直接访问主服务接口,不登录无法访问
2、直接访问自服务,不登录无法访问,(可通过nacos配置用户密码实现登录)
3、主服务通过feign调用子服务接口正常(session已共享)
4、子服务登陆之后,调用主服务理论也可以,需校验下主服务用户侧