Spring Security OAuth2.0 - 学习笔记

news2024/11/24 23:07:17

一、OAuth基本概念

1、什么是OAuth2.0

OAuth2.0是一个开放标准,允许用户授权第三方应用程序访问他们存储在另外的服务提供者上的信息,而不需要将用户和密码提供给第三方应用或分享数据的所有内容。

2、四种认证方式

1)授权码模式

2)简化模式

3)密码模式

4)客户端模式

普通令牌只是一个随机的字符串,没有特殊的意义,当客户带上令牌去访问应用的接口时,应用本身无法判断这个令牌是否正确,就需要到授权服务器上判断令牌。高并发下,检查令牌的网络请求就有可能成为一个性能瓶颈。

改良的方式:JWT令牌,将令牌对应的相关信息全部冗余到令牌本身,这样资源服务器就不再需要发送请求给授权服务器去检查令牌,自己就可以读取到令牌的授权信息。JWT令牌的本质就是一个加密的字符串。

3、联合登录和单点登录

单点登录

联合登录

4、实例流程

用户-百度-微信

官方

  • 客户端(Client):浏览器、微信客户端--本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源
  • 资源拥有者(ResourceOwner):通常是用户,也可以应用程序
  • 授权服务器(AuthorizationServer):用于服务提供者对资源拥有的身份进行认证,对访问资源进行授权,认证成功后会给客户端发放令牌,作为客户端访问资源服务器的凭据。
  • 资源服务器(ResourceServer):存储资源的服务器,例如微信通过OA协议让百度获取到自己存储的用户信息,而百度通过OA协议,让用户可以访问自己的受保护资源。

二、SpringSecurity基本概念

1、认证

用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

2、授权

授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。

3、会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

三、简单的权限模型

1、建表

CREATE TABLE `role`  (
  `id` int NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

INSERT INTO `role` VALUES (1, 'mobile');
INSERT INTO `role` VALUES (2, 'salary');
CREATE TABLE `source`  (
  `id` int NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

INSERT INTO `source` VALUES (1, 'admin', 'mobile');
INSERT INTO `source` VALUES (2, 'admin', 'salary');
INSERT INTO `source` VALUES (3, 'manage', 'mobile');
CREATE TABLE `user`  (
  `id` int NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `pass` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

INSERT INTO `user` VALUES (1, 'admin', 'admin');
INSERT INTO `user` VALUES (2, 'manage', 'manage');

2、搭建-properties依赖、构建实体类、连接数据库

3、实现

1)LoginRequest

@Data
public class LoginRequest {

    @JsonProperty("name")
    private String name;

    @JsonProperty("pass")
    private String pass;

}

2)AuthService

public interface AuthService {

    UserDO userLogin(LoginRequest request);

    List<String> havaPermission(UserDO userDO);

}

3)AuthServiceImpl

@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private SourceMapper sourceMapper;

    @Override
    public UserDO userLogin(LoginRequest request) {
        System.out.println(request);
        UserDO userDO = userMapper.selectOne(new LambdaQueryWrapper<UserDO>()
                .eq(UserDO::getName, request.getName())
                .eq(UserDO::getPass, request.getPass())
        );
        return userDO;
    }

    @Override
    public List<String> havaPermission(UserDO userDO) {
        List<SourceDO> sourceDOS = sourceMapper.selectList(new LambdaQueryWrapper<SourceDO>().eq(SourceDO::getName, userDO.getName()));
        List<String> collect = sourceDOS.stream().map(obj -> obj.getSource()).collect(Collectors.toList());
        return collect;
    }

}

4)SalaryController

@RestController
@RequestMapping("/salary")
public class SalaryController {
    @GetMapping("/query")
    public String query() {
        return "salary";
    }
}

5)MobileController

@RestController
@RequestMapping("/mobile")
public class MobileController {
    @GetMapping("/query")
    public String query() {
        return "mobile";
    }
}

6)LoginController

@Slf4j
@RestController
@RequestMapping("/loginController")
public class LoginController {
    @Resource
    private AuthServiceImpl authService;

    @PostMapping("/login")
    public UserDO login(@RequestBody LoginRequest request,
                        HttpServletRequest httpServletRequest,
                        HttpServletResponse response
    ) {
        UserDO user = authService.userLogin(request);
        if (null != user) {
            log.info("user login succeed");
            httpServletRequest.getSession().setAttribute("currentUser", user);
            System.out.println((httpServletRequest.getSession()));
        }else {
            log.info("user login failed");
        }
        return user;
    }

    @PostMapping("/getCurrentUser")
    public Object getCurrentUser(HttpSession session) {
        return session.getAttribute("currentUser");
    }

    @PostMapping("/logout")
    public void logout(HttpSession session) {
        session.removeAttribute("currentUser");
    }
    @PostMapping("/havaPermission")
    public List<String> havaPermission(UserDO userDO){
        return authService.havaPermission(userDO);
    }
}

7)MyWebAppConfigurer

@Component
public class MyWebAppConfigurer implements WebMvcConfigurer {
    @Resource
    private AuthInterceptor authInterceptor;

    /**
     * 配置权限拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/**");
    }

    /**
     * 简单配置启动页面
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/index.html");
    }
}

8)拦截器 -- 登录以后才可以访问

@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Autowired
    AuthService authService;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler
    ) throws Exception {
        //1、不需要登录就可以访问的路径
        String requestURI = request.getRequestURI();
        if (requestURI.contains(".") || requestURI.startsWith("/" + "loginController")) {
            return true;
        }

        //2、未登录用户,直接拒绝访问
        if (null == request.getSession().getAttribute("currentUser")) {
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("please login first");
            return false;
        } else {
            UserDO currentUser = (UserDO) request.getSession().getAttribute("currentUser");
            List<String> strings = authService.havaPermission(currentUser);
            //3、已登录用户,判断是否有资源访问权限
            if (requestURI.startsWith("/" + "mobile" + "/") && strings.contains("mobile")) {
                return true;
            } else if (requestURI.startsWith("/" + "salary" + "/") && strings.contains("salary")) {
                return true;
            } else {
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write("no auth to visit");
                return false;
            }
        }
    }
}

9)html

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form id="loginForm" method="post">
        <label for="name">name:</label>
        <input type="text" id="name" name="name" required><br>
        <label for="pass">pass:</label>
        <input type="pass" id="pass" name="pass" required><br>
        <button type="submit">Login</button>
    </form>

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="script.js"></script>
</body>
</html>
$(document).ready(function() {
    // 监听表单提交事件
    $("#loginForm").submit(function(event) {
        // 阻止表单默认提交行为
        event.preventDefault();
        // 获取用户名和密码
        var name = $("#name").val();
        var pass = $("#pass").val();

        // 发送登录请求
        $.ajax({
            url: "http://localhost:端口号/loginController/login",
            type: "POST",
            data: JSON.stringify({name: name, pass: pass}),
            contentType: "application/json",
            success: function(response) {
                // 登录成功处理
                console.log("Login successful");
                // 可以在此处跳转到其他页面
            },
            error: function(xhr, status, error) {
                // 登录失败处理
                console.log("Login failed");
                console.log(xhr.responseText);
            }
        });
    });
});

这样一个简单的demo就完成了,接下来测试。

http://localhost:端口号/index.html

进入登录界面后,输入账号密码,这边ajax没有跳转其他界面,只是为了获取Set-Cookie,也就是session,再次访问http://localhost:1223/mobile/query的时候 就可以查看到mobile的信息了。

四、拓展

基于上述的父工程,创建子模块。复用MobileController、SalaryController。

1、依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、注解

启动类加注解@EnableWebSecurity

3、简单的启动

登录用户名是user,密码在启动控制台中会显示。

4、注入密码解析器及用户来源

1)MyWebConfig

通过注入一个PasswordEncoder对象实现密码加密。包括CryptPassEncoder、Argon2PasswordEncoder、Pbkdf2PasswordEncoder等。

通过注入一个UserDetailService来管理系统的实体数据,如果不自己注入,在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的UserDetailService。在SpringSecurity中,也提供了JdbcUserDetailsManager来实现对数据库的用户信息进行管理。

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    /**
     * 默认Url根路径跳转到/login,此url为spring security提供
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

    /**
     * 加密
     *
     * @return
     */
    @Bean
    public PasswordEncoder getPassWordEncoder() {
        return new BCryptPasswordEncoder(10);
//        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 自行注入一个UserDetailsService
     * 如果没有的话,在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的InMemoryUserDetailsManager
     * 另外也可以采用修改configure(AuthenticationManagerBuilder auth)方法并注入authenticationManagerBean的方式。
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {

//        // 创建数据源
//        DataSource dataSource = new DruidDataSource();
//        // 设置数据库连接信息
//        ((DruidDataSource) dataSource).setUrl("jdbc:mysql://localhost:3306/sys");
//        ((DruidDataSource) dataSource).setUsername("root");
//        ((DruidDataSource) dataSource).setPassword("root");
//        ((DruidDataSource) dataSource).setDriverClassName("com.mysql.cj.jdbc.Driver");
//        // 将 DataSource 传递给 JdbcUserDetailsManager 根据接口方式进行拓展,表结构不同
//        return new JdbcUserDetailsManager(dataSource);
//        //自定义
//        return new MyUserService();

        //自定义一个Manager,没连接数据库
        InMemoryUserDetailsManager userDetailsManager =
                new InMemoryUserDetailsManager(User.withUsername("admin")
                        .password(getPassWordEncoder().encode("admin"))
                        .authorities("mobile", "salary")
                        .build(),
                        User.withUsername("manager").password("manager").authorities("salary").build(),
                        User.withUsername("worker").password("worker").authorities("mobile").build());
        return userDetailsManager;
    }
}

 2)MyUserService

public class MyUserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //假数据 需要在myWebConfig中注入
       if("admin".equals(username)){
           return User.withUsername("admin")
                   .password("admin")
                   .authorities("mobile", "salary")
                   .build();
       }
       return null;
    }
}

 5、注入校验配置规则

 MyWebSecurityConfig

@EnableWebSecurity
//public class MyWebSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //关闭csrg跨域检查
        http.csrf().disable()
                .authorizeRequests()
                //配置资源权限
                .antMatchers("/**.html", "/css/**").permitAll()
                .antMatchers("/mobile/**").hasAuthority("mobile")
                .antMatchers("/salary/**").hasAuthority("salary")
                //loginController下的请求直接通过
                .antMatchers("/loginController/**").permitAll()
                //其他请求需要登录
                .anyRequest().authenticated()
                .and()
                //记住我(随机的秘钥(强大的随机字符串)、过期时间)
                .rememberMe().userDetailsService(userDetailsService)
                .key("your-remember-me-key")
                .tokenValiditySeconds(86400)
                .and()
                .formLogin()
                //自定义登录页面
//                .loginPage("/index.html").loginProcessingUrl("/login")
                .defaultSuccessUrl("/SuccessUrl.html")
                //可从默认的login页面登录,并且登录后跳转到main.html
                .failureUrl("/SuccessUrl.html");
    }
}

自定义登录

http.loginPage()方法配置登录页,http.loginProcessingUrl()方法定制登录逻辑。登录页的源码DefaultLoginPageGeneratingFilter。

记住我

登录时提交一个remeber-me的参数,值可以是 on 、yes 、1 、 true,就会记住当前登录用户的token到cookie中。在登出时,会清除记住我功能的cookie。

拦截策略

antMachers()方法设置路径匹配,可以用两个星号代表多层路径,一个星号代表一个或多个字符,问号代表一个字符。

配置对应的安全策略

permitAll()所有人都可以访问。

denyAll()所有人都不能访问。

anonymous()只有未登录的人可以访问,已经登录的无法访问。

hasAuthority、hasRole这些是配置需要有对应的权限或者角色才能访问。

AuthenticationManagerBuilder配置认证策略,WebSecurity配置补充的Web请求策略。

csrf

Cross—Site Request Forgery 跨站点请求伪造。这是一种安全攻击手段,简单来说,就是黑客可以利用存在客户端的信息来伪造成正常客户,进行攻击。例如你访问网站A,登录后,未退出又打开一个tab页访问网站B,这时候网站B就可以利用保存在浏览器中的sessionId伪造成你的身份访问网站A。我们在示例中是使用http.csrf().disable()方法简单的关闭了CSRF检查。而其实Spring Security针对CSRF是有一套专门的检查机制的。他的思想就是在后台的session中加入一个csrf的token值,然后向后端发送请求时,对于GET、HEAD、TRACE、OPTIONS以外的请求,例如POST、PUT、DELETE等,会要求带上这个token值进行比对。当我们打开csrf的检查,再访问默认的登录页时,可以看到在页面的登录form表单中,是有一个name为csrf的隐藏字段的,这个就是csrf的token。例如我们在freemarker的模板语言中可以使用添加这个参数。而在查看Spring Security后台,有一个CsrfFilter专门负责对Csrf参数进行检查。他会调用HttpSessionCsrfTokenRepository生成一个CsrfToken,并将值保存到Session中。

注解级别方法支持 :

在@Configuration支持的注册类上打开注解

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled =true,jsr250Enabled = true

prePostEnabled属性对应@PreAuthorize。

securedEnabled 属性支持@Secured注解,支持角色级别的权限控制。

jsr250Enabled属性对应@RolesAllowed注解,等价于@Secured。

异常处理

现在前后端分离的状态可以使用@ControllerAdvice注入一个异常处理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。

 6、获取当前用户信息

@Slf4j
@RestController
@RequestMapping("/loginController")
public class LoginController {

    @GetMapping("/getLoginUserByPrincipal")
    public String getLoginUserByPrincipal(Principal principal) {
        return principal.getName();
    }

    @GetMapping(value = "/getLoginUserByAuthentication")
    public String currentUserName(Authentication authentication) {
        return authentication.getName();
    }

    @GetMapping(value = "/username")
    public String currentUserNameSimple(HttpServletRequest request) {
        Principal principal = request.getUserPrincipal();
        return principal.getName();
    }

    @GetMapping("/getLoginUser")
    public String getLoginUser() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getUsername();
    }
}

 五、工作原理

 1、结构总结

Spring Security是解决安全访问控制的问题,就是认证和授权。

Spring Security的重点是对所有进入系统的请求进行拦截,校验每个请求是否能够访问所期望的资源,对web资源的保护是通过Filter来实现的。

当初始化Spring Security时,在org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration中会往spring容器中注入一个SpringSecurityFilterChain的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求都会经过这个类。

而FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的所有Filter,同时这些Filter都已经注入到Spring容器中。但是他们并不直接处理用户的认证和授权,而是把其交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。

 Spring Security的功能实现主要就是一系列过滤器链相互配合完成的。

SecurityContextPersistenceFilter :

整个拦截过程的入口和出口,在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,再设置给SecurityContextHolder。请求完成后将SecurityContextHolder持有的SecurityContext再保存到SecurityContextRepository,同时清除SecurityContextHolder所持有的securityContext。 

UsernamePasswordAuthenticationFilter : 

用于处理来自表单提交的认证,表单必须提供对应的用户名和密码,内部还有登录成功或失败后进行处理的AuthenticationSuccessHandlerAuthenticationFailureHandler

FilterSecurityInterceptor :

用于保护web资源,使用AccessDecisionManager对当前用户进行授权访问

ExceptionTranslationFilter :

能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationExceptionAccessDeniedException,其它的异常它会继续抛出

2、认证流程

1)用户提交用户名、密码,SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为Authentication

2)过滤器将Authentication提交到认证管理器(AuthenticationManager)进行认证。

3)认证成功后,认证管理器(AuthenticationManager)返回一个被填充信息的Authentication实例。

4)SecurityContextHolder安全上下文容器将第三步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)赋值到其中。可以看出AuthenticationManager接口是发起认证的出发点,实现类为ProviderManager,而Spring Security支持多种认证方式,因此ProviderManager维护着一个List 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication

3、授权流程

1、整体流程

1)拦截请求

已认证用户访问受保护的web资源将被SecurityFilterChain中(实现类为DefaultSecurityFilterChain)的 FilterSecurityInterceptor 的子类拦截。

2)获取资源访问策略

FilterSecurityInterceptor会从 SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需的权限Collection

3)决策

FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。关于AccessDecisionManager接口,最核心的就是其中的decide方法。这个方法就是用来鉴定当前用户是否有访问对应受保护资源的权限。

2、决策流程

在AccessDecisionManager的实现类ConsensusBased中,是使用投票的方式来确定是否能够访问受保护的资源。

AccessDecisionManager中包含了一系列的AccessDecisionVoter讲会被用来对Authentication是否有权访问受保护对象进行投票,根据投票结果,做出最终角色。

为什么要投票呢?

权限可以从多个方面进行配置,有角色但是没有资源怎么办呢,就需要有不同的处理策略。

AccessDecisionVoter定义了三个方法,赞成、拒绝、弃权。

1)AffirmativeBased 默认:只要有一个投票通过,就表示通过。

  • 只要有一个投票通过了,就表示通过。
  • 如果全部弃权也表示通过。
  • 如果没有人投赞成票,但是有人投反对票,则抛出AccessDeniedException.

2)ConsensusBased:多数赞成就通过

  • 如果赞成票多于反对票则表示通过
  • 如果反对票多于赞成票则抛出AccessDeniedException
  • 如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则抛出AccessDeniedException。默认是true。
  • 如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。默认为false。

3)UnanimousBased:一票否决。

  • 如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException
  • 如果没有反对票,但是有赞成票,则表示通过。
  • 如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。Spring Security默认是使用的AffirmativeBased投票器,我们同样可以通过往Spring容器里注入的方式来选择投票决定器

选择投票器

@Bean
public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
        new WebExpressionVoter(),
        new RoleVoter(),
        new AuthenticatedVoter(),
        new MinuteBasedVoter()
    );
    return new UnanimousBased(decisionVoters);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.    
    //.其他参数
    .anyRequest()
    .authenticated()
    .accessDecisionManager(accessDecisionManager());
}

3、总结

4、自定义认证

1、自定义登录页面及过程

2、数据源改为数据库获取

这两点在上述都有代码及注释展示。

3、配置方法与资源绑定关系

1)代码方式

authenticated()                 //保护URL,需要用户登录
permitAll()                     //指定URL无需保护,一般应用与静态资源文件
hasRole(String role)            //限制单个角色访问。角色其实相当于一个"ROLE_"+role的资源。
hasAuthority(String authority)  //限制单个权限访问
hasAnyRole(String… roles)       //允许多个角色访问.
hasAnyAuthority(String… authorities)       //允许多个权限访问.
access(String attribute)                    //该方法使用 SpEL表达式, 所以可以创建复杂的限制.
hasIpAddress(String ipaddressExpression)    //限制IP地址或子网

2)注解方式

  •  启动类上加入@EnableGlobalMethodSecurity(securedEnabled=true),开启注解过滤权限
  • 权限的方法上使用@Secured(Resource),匿名登录IS_AUTHENTICATED_ANONYMOUSLY
  • @EnableGlobalMethodSecurity(jsr250Enabled=true) 开启@RolesAllowed 注解过滤权限
  • @EnableGlobalMethodSecurity(prePostEnabled=true)使用表达式时间方法级别的安全性,打开后可以使用以下几个注解。
    • @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问。例如@PreAuthorize("hasRole('normal') AND hasRole('admin')")
    • @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常。此注释支持使用returnObject来表示返回的对象。例如@PostAuthorize("returnObject!=null && returnObject.username == authentication.name")
    • @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
    • @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

5、会话控制

1、获取当前用户信息

 用户认证通过之后,为了避免每次操作都要进行认证,将用户的信息保存在会话中。SpringSecurity提供了会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,它与当前线程进行绑定,获取用户身份。通过SecurityContextHolder.getContext().getAuthentication()获取信息。

2、会话控制

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) 
}
机制    描述
always  如果没有session就创建一个
ifRequired  如果需要在登录时创建一个(默认)
never  不会创建,但是如果其他应用中创建了session,也会使用
stateless  绝对不创建session,也不会使用

3、会话超时

在properties文件可直接配置

server.servlet.session.timeout=3600s

如果会话超时,配置跳转地址

http.sessionManagement()
//session过期
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
//传入的sessionId失效
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");

4、安全会话cookie

在properties文件可直接配置

//true - 浏览器脚本无法访问cookie
server.servlet.session.cookie.http‐only=true
//true - cookie仅通过HTTPS链接发送
server.servlet.session.cookie.secure=true

5、退出

http
.and()
//提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
.logout()
//默认退出地址
.logoutUrl("/logout") 
//退出后的跳转地址
.logoutSuccessUrl("/login‐view?logout")
//添加一个LogoutHandler,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler会被添加为最后一个LogoutHandler
.addLogoutHandler(logoutHandler) 
//指定是否在退出时让HttpSession失效,默认是true
.invalidateHttpSession(true); 

六、分布式系统认证方案

1、分析

1)统一认证授权

无论不同类型的用户,还是不同类型的客户端,采用一致的认证、授权、会话判断机制,实现统一认证授权服务。

要实现这种统一的认证方式必须可拓展,支持各种认证需求,例如密码、二维码等。

2)多样的认证场景

例如各种支付之间有不同的安全级别,需要对应不同的认证场景。

2)应用接入认证

提供扩展和开放的能力,提供安全的系统对接机制,并且可开放部分API给第三方使用,内部服务和外部第三方服务采用统一的接入机制。

2、分布式认证方案

基于Session和基于Token

1)基于Session

由服务端保存统一的用户信息,只是在分布式环境下,将session信息同步到各个服务,并对请求进行均衡的负载

  • session复制

   在多台应用服务器之间同步session,并使session保持一致,对外透明。

  • session黏贴

   当用户访问集群中某台服务器后,强制指定后续所有请求都落到此机器。

  • session集中存储

   将session存入分布式缓存中,所有服务器应用实例都统一从分布式缓存中获取session信息。

基于session认证的方式,可以更好的在服务端对会话进行控制,并且安全性较高。但是,session机制总体是基于cookie的,客户端需要保存sessionI的,这样在复杂的客户端上不能有效的使用。随着系统的扩展需要提高session的复制、黏贴、存储的容错性。

2)基于Token

服务器不再存储认证数据,易维护,扩展性强,客户端可以把token存在任意地方,并且可以实现web和app统一认证机制。

但是,客户端信息容易泄露,token包含了大量信息,因此一般数据量较大,而且每次请求需要传递,也占宽带。并且token的签名验签操作也会带来负担。

3、选择

通常下,选择token的方式,可以保证整个系统更灵活的拓展性,并且减轻服务端的压力。

在这种情况下,一般会独立出统一认证服务(UAA)和网关两个部分来一起完成认证授权服务。

  • 统一认证服务承载接入方认证、登入、授权以及令牌管理,完成实际的用户认证、授权功能。
  • 网关会作为整个分布式系统的唯一入口,为接入方提供API结合。本身也具有辅助,例如监控、负载均衡、缓存、协议转换等功能。核心在于,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所有与业务无关的功能。

七、Spring Security OAuth2.0

1、依赖

基于上述工程的父依赖,创建子模块security-uaa和security-salary

主要是以下四个:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
    <groupId>javax.interceptor</groupId>
    <artifactId>javax.interceptor-api</artifactId>
</dependency>

2、security-uaa

1)配置文件

server.port=1226

spring.application.name=uaa‐service
server.servlet.context‐path=/uaa

2)启动类注解


@SpringBootApplication
@EnableAuthorizationServer
public class SecurityUaaApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityUaaApplication.class, args);
    }

}

3)TokenConfig

@Configuration
public class TokenConfig {

    //签名key
    private static final String SIGN_KEY="uaa";

    @Bean
    public TokenStore tokenStore(){
        //使用基于内存的普通令牌
//        return new InMemoryTokenStore();
        //基于JWT令牌
//        return new JwtTokenStore(accessTokenConvert());
    }

//    @Bean
//    public JwtAccessTokenConverter accessTokenConvert(){
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(SIGN_KEY);
//        return converter;
//    }

}

4)AuthorizationConfig

@Configuration
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //3、配置令牌端点的安全约束
        security
                //oauth/token_key公开
                .tokenKeyAccess("permitAll()")
                //oauth/check_token公开
                .checkTokenAccess("permitAll()")
                //表单认证,申请令牌
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
        //1、客户端详情服务,用户信息或者数据库配置
        clientDetails
                //内存方式配置用户信息
                .inMemory()
                //clientId
                .withClient("c1")
                //客户端秘钥
                .secret(new BCryptPasswordEncoder().encode("secret"))
                //资源列表
                .resourceIds("salary","mobile")
                //该client允许的授权类型
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                //允许的授权范围
                .scopes("all")
                //跳转到授权界面
                .autoApprove(false)
                //回调地址
                .redirectUris("https://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //2、配置令牌的访问端点和令牌服务
        endpoints
                //定制授权同意页面
                .pathMapping("/oauth/confirm_access", "/custom/confirm_access")
                //认证管理器
                .authenticationManager(authenticationManager)
                //密码模式的用户信息管理
                .userDetailsService(userDetailsService)
                //授权码服务
                .authorizationCodeServices(authorizationCodeServices())
                //令牌管理服务
                .tokenServices(tokenService())
                //允许请求方式
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }


    /**
     * token配置
     *
     * @return
     */
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        //客户端详情服务
        service.setClientDetailsService(clientDetailsService);
        //允许令牌自动刷新
        service.setSupportRefreshToken(true);
        //令牌存储策略-内存
        service.setTokenStore(tokenStore);
        //使用jtw
        service.setTokenEnhancer(jwtAccessTokenConverter);
        //令牌默认有效期2小时
        service.setAccessTokenValiditySeconds(7200);
        //刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }

    /**
     * 设置授权码模式的授权码如何存取,暂时用内存方式。
     *
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }


}

核心步骤:

1)ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化。能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),负责查找ClientDetails,一个ClientDetails代表一个需要接入的第三方应用。

  • clientId: 用来标识客户的ID
  • secret: 客户端安全码
  • scope: 用来限制客户端的访问范围,如果是空(默认)的话,那么客户端拥有全部的访问范围。
  • authrizedGrantTypes:客户端可以使用的授权类型,默认为空。
  • authorities:客户端可以使用的权限
  • redirectUris:回调地址

2)AuthorizationServerEndpointsConfifigurer:用来配置令牌(token)的访问端点和令牌服务(tokenservices)。

令牌服务:

实现一个AuthorizationServerTokenServices这个接口,需要继承DefaultTokenServices这个类。可以使用它来修改令牌的格式和令牌的存储。默认情况下在创建一个令牌时,是使用随机值来进行填充的。这个类中完成了令牌管理的几乎所有的事情,唯一需要依赖的是spring容器中的一个TokenStore接口实现类来定制令牌持久化。

  • InMemoryTokenStore:默认方式。适合在单服务器上运行(即并发访问压力不大的情况下,并且他在失败时不会进行备份)。大多数的项目都可以使用这个实现类来进行尝试。也可以在并发的时候来进行管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore:基于JDBC的实现类,令牌会被保存到关系型数据库中。使用这个实现类,可以在不同的服务器之间共享令牌信息。类似还有RedisTokenStore基于Redis存储令牌信息。
  • JwtTokenStore(JSON Web Token):把令牌信息全部编码整合进令牌本身,这样后端服务可以不用存储令牌相关信息。缺点, 那就是撤销一个已经授权的令牌非常困难。通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。并且令牌会比较大,因为他要包含较多的用户凭证信息。

访问端点:

配置授权类型(Grant Types)

  • authenticationManager:认证管理器。当选择password(资源所有者密码)这个授权类型时,就需要指定authenticationManager对象来进行鉴权。
  • userDetailsService:用户主体管理服务。如果设置这个属性,说明有一个自定义的UserDetailsService接口的实现,或者你可以设置到全局域(例如GlobalAuthenticationManagerConfigurer)上去,当设置后,那么refresh_token刷新令牌方式的授权类型流程中就会多包含一个检查步骤,来确保这个账号是否仍然有效。
  • authorizationCodeServices:用来设置授权服务器的,主要用于authorization_code 授权码类型模式。
  • implicitGrantService:用于设置隐式授权模式的状态。
  • tokenGranter:如果设置了这个东东(即TokenGranter接口的实现类),那么授权将会全部由自己掌控,并且会忽略掉以上几个属性。这个属性一般是用作深度拓展用途。

配置授权断点的URL(Endpoint URLS)

  • 可以通过pathMapping()方法来配置断点URL的链接地址。即将oauth默认的连接地址替代成其他的URL链接地址。例如spring security默认的授权同意页面/auth/confirm_access,就可以通过passMapping()方法映射成自己定义的授权同意页面。

3)AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.

 5)web安全配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 从父类加载认证管理器
     *
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager =
                new InMemoryUserDetailsManager(User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("mobile", "salary").build()
                , User.withUsername("manager").password(passwordEncoder().encode("manager")).authorities("salary").build()
                , User.withUsername("worker").password(passwordEncoder().encode("worker")).authorities("worker").build());
        return userDetailsManager;
    }

    /**
     * 配置用户的安全拦截策略
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //链式配置拦截策略
        http.csrf().disable()//关闭csrf跨域检查
                .authorizeRequests()
                .anyRequest().authenticated() //其他请求需要登录
                .and() //并行条件
                .formLogin(); //可从默认的login页面登录,并且登录后跳转到
        }

}

 6)测试

  • 客户端模式

客户端向授权服务器发送自己的身份信息,请求令牌access_token,直接返回

  •  密码模式

    

  •  简化模式:

http://localhost:1226/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=https://www.baidu.com

配置自己的端口号,请求路径,client_id、type、scope、uri,会自动跳转到请求登录界面,选择approve后,跳转到baidu

  • 授权码模式

http://localhost:1226/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

跳转到baidu,拿到code,再请求

  • 验证令牌

3、security-salary

1)注解

@EnableResourceServer
@SpringBootApplication
public class SecuritySalaryApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecuritySalaryApplication.class, args);
    }

}

2)ResourceServerConfig

@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_SALARY = "salary";

    //使用JWT令牌,需要引入与uaa一致的tokenStore,存储策略
    @Autowired
    private TokenStore tokenStore;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                //资源ID
                .resourceId(RESOURCE_SALARY)
                //使用远程服务验证令牌的服务
//                .tokenServices(tokenServices())
                //使用jwt令牌
                .tokenStore(tokenStore)
                //无状态模式
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                //校验请求
                .authorizeRequests()
                // 路径匹配规则
                .antMatchers("/salary/**")
                // 需要匹配scope
                .access("#oauth2.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        ;
    }

    /**
     * 配置access_token远程验证策略
     *
     * @return
     */
    public ResourceServerTokenServices tokenServices() {
        // DefaultTokenServices services = new DefaultTokenServices();
        RemoteTokenServices services = new RemoteTokenServices();
        services.setCheckTokenEndpointUrl("http://localhost:1226/uaa/oauth/check_token");
        services.setClientId("c1");
        services.setClientSecret("secret");
        return services;
    }
}

public ResourceServerTokenServices tokenServices()如果资源服务和授权服务是在同一个应用程序上,那可以使用DefaultTokenServices,这样的话,就不用考虑关于实现所有必要的接口一致性的问题。而如果资源服务器是分离的,那就必须要保证能够有匹配授权服务提供的ResourceServerTokenServices,知道如何对令牌进行解码。

3)TokenConfig

@Configuration
public class TokenConfig {

    //签名key
    private static final String SIGN_KEY="uaa";

    @Bean
    public TokenStore tokenStore(){
        //使用基于内存的普通令牌
//        return new InMemoryTokenStore();
        //基于JWT令牌
        return new JwtTokenStore(accessTokenConvert());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConvert(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGN_KEY);
        return converter;
    }

}

4)WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/salary/**")
                 .hasAuthority("salary")
                .anyRequest().permitAll();
    }

}

 5)SalaryController

@RestController
@RequestMapping("/salary")
public class SalaryController {
    @GetMapping("/query")
    public String query() {
        return "salary";
    }
}

 6)测试

 八、JWT令牌

一个开放的行业标准(RFC 7519),它定义了一种简单的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名,可以被验证和信任。JWT可以使用HMAC算法或使用RSA算法的公私钥来签名,方式被篡改。

JWT令牌的优点:

基于json,非常方便解析

可以在令牌中自定义内容,易扩展

通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高

资源服务使用JWT可以不依赖于认证服务,自己完成解析

缺点:

令牌较长,占据的存储空间比较大

组成:

  • Header

头部包括令牌的类型(JWT)以及使用的哈希算法(如HMAC SHA256 RSA)

  • Payload

负载,内容也是一个对象,存放有效信息的地方,可以存放JWT提供的现有字段,例如 iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可以自定义字段。不建议存放敏感信息,因为可以解码还原出原始内容。最后将这部分JSON内容使用Base64URL编码,就得到了JWT令牌的第二个部分。

  • Signature

签名,用于防止JWT内容被篡改。这个部分使用Base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明的签名算法进行签名。

代码在上一个部分展示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/785424.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云性能监控的应用是提升云服务质量的关键

随着云计算的普及和企业对云服务的广泛应用&#xff0c;保证云服务的质量和性能已成为企业的重要课题。在这一背景下&#xff0c;云性能监控应运而生&#xff0c;成为提升云服务质量的重要工具&#xff0c;也可以说云性能监控的应用是提升云服务质量的关键。 首先&#xff0c;云…

win10 hadoop报错 unable to load native-hadoop library

win10 安装hadoop执行hdfs -namenode format 和运行hadoop的start-all报错 unable to load native-hadoop library 验证&#xff1a; hadoop checknative -a 这个命令返回都是false是错的 返回下图是正确的 winutils: true D:\soft\hadoop-3.0.0\bin\winutils.exe Native li…

LLM-Blender:大语言模型也可以进行集成学习

最近在看arxiv的时候发现了一个有意思的框架&#xff1a;LLM-Blender&#xff0c;它可以使用Ensemble 的方法来对大语言模型进行集成。 官方介绍如下&#xff1a;LLM-Blender是一个集成框架&#xff0c;可以通过利用多个开源大型语言模型(llm)的不同优势来获得始终如一的卓越性…

pycharm 添加pyuic 插件

添加pyuic插件&#xff0c;就能将ui类型的文件转换称py格式的文件 进入主界面 打开文件->设置->外部工具 点击加号&#xff0c;添加扩展 图1 添加外部扩展 名字记作 pyuic&#xff0c;分组到External Tools 描述为ui to py ,地址选择为 python.exe 的目录地址 参数填写&a…

Python实现抽象工厂模式

抽象工厂模式是一种创建型设计模式&#xff0c;用于创建一系列相关或依赖对象的家族&#xff0c;而无需指定具体类。在Python中&#xff0c;可以通过类和接口的组合来实现抽象工厂模式。 下面是一个简单的Python实现抽象工厂模式的示例&#xff1a; # 抽象产品接口 class Abs…

客户案例 | 思腾合力服务器助力西安电子科技大学人工智能实验室建设

客户介绍 西安电子科技大学是以信息与电子学科为主&#xff0c;工、理、管、文多学科协调发展的全国重点大学&#xff0c;直属教育部&#xff0c;是国家“优势学科创新平台”项目和“211工程”项目重点建设高校之一、国家双创示范基地之一、首批35所示范性软件学院、首批9所示范…

微服务系列(1)-who i am?

微服务系列&#xff08;1&#xff09;-我是谁 应用架构的演化 简单来说系统架构可以分为以下几个阶段&#xff1a;复杂的臃肿的单体架构-SOA架构-微服务 单体架构及其所面临的问题 在互联网发展初期&#xff0c;用户数量少&#xff0c;流量小&#xff0c;硬件成本高。因此…

LangChain Agents深入剖析及源码解密上(一)

LangChain Agents深入剖析及源码解密上(一) LangChain Agents深入剖析及源码解密上 Agent工作原理详解 本节会结合AutoGPT的案例,讲解LangChain代理(Agent)为核心的内容。我们前面已经谈了代理本身的很多内容,也看了绝大部分的源代码,例如:ReAct的源代码,还有mrkl的源代…

HDFS基本操作命令

这里写目录标题 HDFS Shell CLI客户端说明常用命令hadoop fs -mkdir [-p] <path>hadoop fs -ls [-h] [-R] [<path>...]上传文件到指定目录下方法一:hadoop fs -put [-f] [-p] <localsrc>.....<dst>方法二&#xff1a;hadoop fs -moveFromLocal <loc…

金融机构如何管理UPS设备?这个方法超值!

UPS监控在金融行业中扮演着至关重要的角色&#xff0c;确保金融机构在电力故障或波动的情况下依然能够保持业务的稳定运行。 因此&#xff0c;UPS监控在金融行业中扮演着守护者的角色&#xff0c;确保金融机构能够在复杂的电力环境中持续稳健地运行。 客户案例 一家国际性的金…

数据库应用:Redis安装部署

目录 一、理论 1.缓存 2.关系型数据库与非关系型数据库 3.Redis 4.Redis安装部署 5.Redis命令工具 6.Redis数据库常用命令 7.Redis多数据库操作 二、实验 1.Redis安装部署 2.Redis命令工具 3.Redis数据库命令 4.Redis多数据库操作 三、问题 1.RESP连接CentOS 7 R…

KnowStreaming系列教程第二篇——项目整体架构分析

一、KS项目代码结构&#xff1a; ks项目代码结构如上&#xff1a; (1)km-console 是前端部分&#xff0c;基于React开发 (2)km-rest 是后端部分&#xff0c;主要是接受前端请求&#xff0c;对应controller相关代码所在模块 (3)km-biz:业务逻辑处理 (4)km-core:核心逻辑 (5…

MapGIS“透明”地下空间,助力城市纵向发展新实践

城市地下空间拓展是城市化发展的必然趋势&#xff0c;城市化发展需要拓展城市空间&#xff0c;城市空间的横向拓展以蔓延式、平面化的发展模式造成土地资源的巨大浪费&#xff0c;地下空间是城市战略性新型国土资源&#xff0c;开发利用城市地下空间是提高土地利用效率、扩大城…

P2096 最佳旅游线路

竖直方向可以随便走嘛&#xff0c;所以求出每一列 的最大值&#xff0c;再做比较就可以了(求最大子段和)。 ACcode: #include<bits/stdc.h> using namespace std; int n,m,a[105][20010],b[20010],dp[20010]; void solve() {cin>>n>>m;for(int i1;i<n;i)f…

linux服务器部署

文章目录 一、基本工具安装1.使用vi命令编辑文件 二、安装1.jdk2.读入数据 总结 一、基本工具安装 1.使用vi命令编辑文件 注:如果vi命令没有&#xff0c;可以使用yum -y install vim或者apt-get install vim命令安装。 Linux操作系统第二讲 二、安装 1.jdk 参考 卸载jdk…

Spring MVC 是什么?

一、什么是 Spring MVC&#xff1f; 官方对于 Spring MVC 的描述是这样的&#xff1a; Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web …

Robust Unsupervised StyleGAN Image Restoration总结整理

鲁棒的无监督StyleGAN图像恢复 一.创新点 现有的无监督方法必须针对每个任务和降级级别进行仔细调整。&#xff08;这里每个任务都是什么&#xff1f;降级级别是什么&#xff1f;&#xff09; 在这里使用StyleGAN图像恢复健壮&#xff0c;即单组超参数在宽范围的退化水平上起…

[Angular] 主从表结构,从表记录在主表固定栏位上呈现

Background 主从表结构&#xff0c;有时为了方便数据呈现&#xff0c;在UI上不显示从表资料&#xff0c;那么需要动态把从表的资料加载到主表的固定栏位上。 例如&#xff1a;主表是人员信息&#xff0c;从表是银行卡信息&#xff0c;一个人在同一家银行可能有多张银行卡&…

Kubernetes.Service—使用源 IP

使用源 IP 运行在 Kubernetes 集群中的应用程序通过 Service 抽象发现彼此并相互通信&#xff0c;它们也用 Service 与外部世界通信。 本文解释了发送到不同类型 Service 的数据包的源 IP 会发生什么情况&#xff0c;以及如何根据需要切换此行为。 准备开始 术语表 本文使用…

大模型开发(十二):Function calling 流程优化并实现多轮对话任务

全文共1w余字&#xff0c;预计阅读时间约25~40分钟 | 满满干货(附代码案例)&#xff0c;建议收藏&#xff01; 本文目标&#xff1a;围绕Chat模型的Function calling功能进行更高层次的函数封装&#xff0c;并实现一个能够调用外部函数的多轮对话任务 写在前面&#xff1a;本文…