一、背景
随着项目越来越大,需要将多个服务拆分成微服务,使代码看起来不要过于臃肿,庞大。微服务之间通常采取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 {//注入密码加密的类@Beanpublic AuthenticationProvider authenticationProvider() {AuthenticationProvider authenticationProvider = new EncoderProvider();return authenticationProvider;}@Autowiredpublic void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);auth.authenticationProvider(authenticationProvider());}public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);@Overrideprotected 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();}@Autowiredprivate FindByIndexNameSessionRepository sessionRepository;@Beanpublic SpringSessionBackedSessionRegistry sessionRegistry(){return new SpringSessionBackedSessionRegistry(sessionRepository);}}
EncoderProvider.java
@Service
public class EncoderProvider implements AuthenticationProvider {public static final Logger logger = LoggerFactory.getLogger(EncoderProvider.class);/*** 自定义验证方式*/@Overridepublic 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());}}@Overridepublic boolean supports(Class<?> arg0) {return true;}
子服务SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {//注入密码加密的类@Beanpublic AuthenticationProvider authenticationProvider() {AuthenticationProvider authenticationProvider = new EncoderProvider();return authenticationProvider;}@Autowiredpublic void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(authenticationProvider());}public static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);@Overrideprotected 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();}@Autowiredprivate FindByIndexNameSessionRepository sessionRepository;@Beanpublic 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;/*** 自定义验证方式*/@Overridepublic 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());}}@Overridepublic boolean supports(Class<?> arg0) {return true;}}
4、主服务feign配置
FeignManage.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) ;}
public class FeignManageFallback implements FeignManage{@Overridepublic ApiBaseMessage fileUpload(MultipartFile file, String type) {return ApiBaseMessage.getOperationSucceedInstance("400","失败");}}
FeignFileManageFallback.java
FeignConfiguration.java
@Configuration
@Import(FeignClientsConfiguration.class)
public class FeignConfiguration {/**删除请求头文件*/final String[] copyHeaders = new String[]{"transfer-encoding","Content-Length"};@Beanpublic FeignFileManageFallback echoServiceFallback(){return new FeignFileManageFallback();}@Beanpublic FeignBasicAuthRequestInterceptor getFeignBasicAuthRequestInterceptor(){return new FeignBasicAuthRequestInterceptor();}/*** feign 调用,添加CurrentUser*/private class FeignBasicAuthRequestInterceptor implements RequestInterceptor {@Overridepublic 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
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、子服务登陆之后,调用主服务理论也可以,需校验下主服务用户侧