微服务Redis-Session共享登录状态

一、背景

随着项目越来越大,需要将多个服务拆分成微服务,使代码看起来不要过于臃肿,庞大。微服务之间通常采取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、子服务登陆之后,调用主服务理论也可以,需校验下主服务用户侧

相关推荐
茶馆大橘12 分钟前
微服务系列五:避免雪崩问题的限流、隔离、熔断措施
java·jmeter·spring cloud·微服务·云原生·架构·sentinel
北漂IT民工_程序员_ZG35 分钟前
k8s集群安装(minikube)
云原生·容器·kubernetes
coding侠客40 分钟前
揭秘!微服务架构下,Apollo 配置中心凭啥扮演关键角色?
微服务·云原生·架构
lipviolet2 小时前
架构系列---高并发
架构
Phodal2 小时前
架构赋能 AI:知识工程推动下的软件架构数字化
人工智能·架构
lexusv8ls600h2 小时前
微服务设计模式 - 网关路由模式(Gateway Routing Pattern)
spring boot·微服务·设计模式
曹申阳4 小时前
2. JVM的架构模型和生命周期
jvm·架构
车载诊断技术5 小时前
电子电气架构 --- 整车控制系统
网络·架构·汽车·soa·电子电器架构
一叶飘零_sweeeet5 小时前
Dubbo 构建高效分布式服务架构
分布式·架构·dubbo
数据智能老司机5 小时前
LLM工程师手册——监督微调
深度学习·架构·llm