Spring Security OAuth2.0 - 学习笔记

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一、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、实现

1LoginRequest

@Data
public class LoginRequest {

    @JsonProperty("name")
    private String name;

    @JsonProperty("pass")
    private String pass;

}

2AuthService

public interface AuthService {

    UserDO userLogin(LoginRequest request);

    List<String> havaPermission(UserDO userDO);

}

3AuthServiceImpl

@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;
    }

}

4SalaryController

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

5MobileController

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

6LoginController

@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);
    }
}

7MyWebAppConfigurer

@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;
            }
        }
    }
}

9html

<!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、注入密码解析器及用户来源

1MyWebConfig

通过注入一个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;
    }
}

 2MyUserService

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实例。

4SecurityContextHolder安全上下文容器将第三步填充了信息的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定义了三个方法赞成、拒绝、弃权。

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

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

2ConsensusBased多数赞成就通过

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

3UnanimousBased一票否决。

  • 如果受保护对象配置的某一个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);
    }

}

3TokenConfig

@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;
//    }

}

4AuthorizationConfig

@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();
    }


}

核心步骤

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

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

2AuthorizationServerEndpointsConfifigurer用来配置令牌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()方法映射成自己定义的授权同意页面。

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

 5web安全配置

@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);
    }

}

2ResourceServerConfig

@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知道如何对令牌进行解码。

3TokenConfig

@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;
    }

}

4WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

}

 5SalaryController

@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中声明的签名算法进行签名。

代码在上一个部分展示。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring