开发笔记 | 认证授权+Spring Security+OAuth2快速学习笔记

本文仅为个人学习笔记,通过简单的框架搭建来初步学习Spring Security及OAuth2,文中部分方法注解已过时,但仅为学习使用

基础概念

认证:认证用户的合法性,如登录

授权:登录后,用户具有什么操作权限

会话:将用户认证信息保存起来,避免重复认证(常见方式session,token等)

整体的思想:登录认证获得一个权限标识与权限相关信息,存入会话中(session,结合redis等),使得在有效期内无需重复登录或者请求接口校验用户的权限

RBAC:Role-Based Access Control 基于角色的访问控制,由用户,角色,权限组成

简单来说就是一个用户,具有什么角色,每个角色具有什么权限,从而决定改用户具有什么权限

基于RBAC的简单例子

基础代码及登录界面

pom增加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

yml配置文件

server:
  port: 5555

spring:
  application:
    name: security-demo
  thymeleaf:
    prefix: classpath:templates/
    suffix: .html

实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String userId;
    private String userName;
    private String password;
    private List<Role> roles = new ArrayList();
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private String roleId;
    private String roleName;
    private List<Resource> resources = new ArrayList();
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Resource {
    private String resourceId;
    private String resourceName;
    private String resourceType;
}

controller

@Controller
@RequestMapping("/auth")
public class LoginController {

    @Resource
    private AuthService authService;

    @GetMapping
    public String test(){
        return "success";
    }
    //首页
    @RequestMapping("/index")
    public String index(){
        return "index";
    }
    //登录
    @RequestMapping("/login")
    public String login(String userName,String password, HttpServletRequest request){
        User query = new User();
        query.setUserName(userName);
        query.setPassword(password);
        User userResult = this.authService.getUser(query);
        if(null == userResult){
           return "failed";
        }
        request.getSession().setAttribute("current_user",userResult);
        return "success";
    }
    //获取当前用户
    @PostMapping("/getCurrUser")
    public User getCurrentUser(HttpSession session){
        return (User)session.getAttribute("current_user");
    }
    //登出
    @PostMapping("logout")
    public ResponseEntity logout(HttpSession session){
        session.removeAttribute("current_user");
        return ResponseEntity.ok("已退出登录");
    }
}

service

@Service
public class AuthService {
    @Resource
    private AuthMapper authMapper;
    //查询用户
    public User getUser(User user){
        User userResult = this.authMapper.getUser(user);
        return userResult;
    }
}

mapper模拟数据库

@Component
public class AuthMapper {
    //模拟数据库
    List<User> userDataBase = Arrays.asList(new User("1","admin","123456",null,null));

    public  User getUser(User user){
        List<User> list = userDataBase.stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
                .collect(Collectors.toList());
        return list.size() > 0 ? list.get(0) : null;
    }
}

简单界面

resources/templates新增界面

登录界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<form action="/auth/login" method="post">
    用户名:<input type="text" name="userName" required><br/>
    密 码:<input type="text" name="password" required><br/>
    <input type="submit" value="登录"> <input type="reset" value="重置">
</form>
</body>
</html>

登录成功界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录成功</title>
</head>
<body>
登录成功
</body>
</html>

登录失败界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>失败</title>
</head>
<body>
账号或者密码失败
</body>
</html>

到此可以启动项目,输入账号密码,界面正常跳转,到此步认证完成

整合权限控制

修改登录成功后的界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录成功</title>
</head>
<body onload="lordData();">
登录成功
<div>当前用户:</div>
<div id="currentUser"></div>
<br/>
<div>获取苹果:</div>
<div id="getApple"></div>

<div>获取香蕉:</div>
<div id="getBanana"></div>

<br/>
<div onclick="logout();">退出登录</div>
<br/>

<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">

    function lordData(){
        currentUser();
        getApple();
        getBanana();
    }

    function logout(){
        $.ajax({
            //请求方式
            type : "POST",
            //请求的媒体类型
            // contentType: "application/json;charset=UTF-8",
            //请求地址
            url : "/auth/logout",
            //数据,json字符串
            data : {},
            //请求成功
            success : function(result) {
                //result = JSON.stringify(result);
                alert(result)
            },
            //请求失败,包含具体的错误信息
            error : function(error){
                alert(error)
            }
        });
    }

    function currentUser() {
       $.ajax({
           type : "POST",
           url : "/auth/getCurrUser",
           data : {},
           success : function (result) {
               result = JSON.stringify(result);
               $('#currentUser').html("success:" + result);
           },
           error : function (error) {
               $('#currentUser').html("error:" + error);
           }
       })
   }

   function getApple() {
       $.ajax({
           type : "GET",
           url : "/apple",
           data : {},
           success : function (result) {
               $('#getApple').html("success:" + result);
           },
           error : function (error) {
               $('#getApple').html("error:" + error);
           }
       })
   }

   function getBanana() {
       $.ajax({
           type : "GET",
           url : "/banana",
           data : {},
           success : function (result) {
               //result = JSON.stringify(result);
               $('#getBanana').html("success:" + result);
           },
           error : function (error) {
               $('#getBanana').html("error:" + error);
           }
       })
   }
</script>
</body>
</html>

新增两个资源获取接口

@RestController
@RequestMapping("/apple")
public class AppleController {
    @GetMapping
    public String getApple(){
        return "资源1苹果";
    }
}

@RestController
@RequestMapping("/banana")
public class BnanaController {
    @GetMapping
    public String getBanana(){
        return "资源2香蕉";
    }
}

启动项目,登录后,界面会去请求对应的接口获得数据

但此时接口均为做权限的限制,故通过定义适配器+拦截器进行权限的拦截

完善模拟数据库的权限数据

登录成功后,将用户及其权限信息存入session

创建苹果,香蕉资源,再创建管理员角色(苹果,香蕉资源)和苹果经销商角色(苹果资源),

再创建管理员用户(管理员角色(苹果,香蕉资源)),只卖苹果商家用户(苹果经销商角色(苹果资源))

@Component
public class AuthMapper {

    public  User getUser(User user){
        List<User> list = this.getUserList().stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
                .collect(Collectors.toList());
        return list.size() > 0 ? list.get(0) : null;
    }

    //模拟用户数据库
    public List<User> getUserList(){
        
        List<User> users = new ArrayList<>();
        
        //两个资源呢数据
        Resource appleResource = new Resource("1","apple","1");
        Resource bananaResource = new Resource("2","banana","1");

        //创建三种角色 管理员 苹果卖家
        //管理员角色具有苹果 香蕉数据查看权限
        Role adminRole = new Role("1","admin",Arrays.asList(appleResource,bananaResource));
        //苹果卖家只能看到苹果数据的权限
        Role appleRole =  new Role("2","appleRole",Arrays.asList(appleResource));


        //创建两个用户 超级管理员具有admin角色
        User admin = new User("1","admin","123456",Arrays.asList(adminRole));
        users.add(admin);
        //苹果用户 具有 appleRole角色
        User appleOnly =  new User("1","apple","123456",Arrays.asList(adminRole));
        users.add(appleOnly);
        return users;
    }
}

创建应用上下文配置MyWebAppConfigurer

(在此处可配置接口的拦截器)

@Component
public class MyWebAppConfigurer implements WebMvcConfigurer {

    @Resource
    private AuthInterceptor authInterceptor;

    //启动界面的简单配置
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //重定向至主页
        registry.addViewController("/").setViewName("redirect:/index.html");
    }

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

创建拦截器

对登录状态及资源的范问权限进行拦截

@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //设置不需要登录即刻访问的接口
        String url = request.getRequestURI();
        if (url.contains(".") || url.startsWith("/auth/")){
            return true;
        }
        //未登录的用户
        if(null == request.getSession().getAttribute("current_user")){
            response.setCharacterEncoding("UTF-8");
            response.setHeader("content-type","text/html;charset=UTF-8");
            response.getWriter().write("请先登录!");
            return false;
        }else {
            //已登录的用户 进行资源的权限校验
            User user = (User)request.getSession().getAttribute("current_user");
            if(url.startsWith("/apple") && this.hasPermission("apple",user.getRoles())){
                return true;
            }else if (url.startsWith("/banana") && this.hasPermission("banana",user.getRoles())){
                return true;
            }else {
                response.setCharacterEncoding("UTF-8");
                response.setHeader("content-type","text/html;charset=UTF-8");
                response.getWriter().write("暂无权限");
                return false;
            }

        }
    }

    public boolean hasPermission(String type,List<Role> roles){
        for (Role r : roles){
            if(r.getResources().stream().filter(e -> e.getResourceName().equals(type)).count() > 0){
                return true;
            }
        }
        return false;
    }
}

至此将对没登录,登陆后不同用户根据权限进行接口访问限制

admin登录

 apple登录

 未登录访问http://127.0.0.1:5555/banana

 至此一个简单的基于RBAC模型的例子完成,下一步结合Spring Security来优化完善。

Spring Security

配置

pom

创建项目,新增依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

yml

Spring Security不需要配置即可直接启动

server:
  port: 5556

spring:
  application:
    name: spring-security-demo
    thymeleaf:
      prefix: classpath:templates/
      suffix: .html

启动类加上@EnableWebSecurity

controller

同上一个例子一样的苹果,香蕉资源

@RestController
@RequestMapping("/apple")
public class AppleController {
    @GetMapping
    public String getApple(){
        return "资源1苹果";
    }
}

@RestController
@RequestMapping("/banana")
public class BnanaController {
    @GetMapping
    public String getBanana(){
        return "资源2香蕉";
    }
}

此时启动项目,访问这两个接口,均跳转Spring Security自带的登录界面,输入user,密码为控制台日志打印的【Using generated security password】即可登录

认证与授权的实现

修改建应用上下文配置,创建默认用户及权限

通过自定义注入UserDetailsService 用于管理系统的用户账号密码信息,如果不注入,springt security将默认注入一个包含登录名为user的用户(如上),密码打印在控制台

PasswordEncoder密码解析器

spring security要求容器内需要拥有一个PasswordEncoder实例

常用BCryptPasswordEncoder实现类,同一个字符串每次加密得到不同结果

构成:

$2a表示版本 $10 表示算法强度 $xxx 随机盐 xxxx 文本hash值

60位字符串

@Configuration
public class MyWebAppConfigurer implements WebMvcConfigurer {

    //启动界面的简单配置
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //此处修改为重定向至 /login接口 /login由spring security提供
        registry.addViewController("/").setViewName("redirect:/login");
    }

    //免密解析器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder(10);
    }

    /**
     * 设置初始用户来源
     * 自行注入一个UserDetailsService UserDetailsServiceAutoConfiguration中有默认的
     * InMemoryUserDetailsManager
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager =new InMemoryUserDetailsManager(
                User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("apple","salary","ROLE_stu").build(),
                User.withUsername("apple").password(passwordEncoder().encode("apple")). authorities("apple").build(),
                User.withUsername("banana").password(passwordEncoder().encode("banana")).authorities("banana").build());
        return manager;
    }
}

其中,在设置权限时候,用“ROLE_” + 角色表示角色权限,此时ROLE_不能省略

如配置了角色ROLE_stu,在spring security配置文件中,设置角色权限

.antMatchers("/test/**").hasRole("stu")即可,此时也不能带上"ROLE_"

配置WebSecurityConfig配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //定义安全拦截策略
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf().disable() //关闭csrf跨站攻击防御
                .authorizeRequests()
                .antMatchers("/apple/**").hasAuthority("apple") // 配置拦截路径及资源权限
                .antMatchers("/banana/**").hasAuthority("banana")
                .antMatchers("/auth/**").permitAll() //登录相关接口不进行拦截
                .anyRequest().authenticated() //其他请求都需要登录
                .and()
                .formLogin().defaultSuccessUrl("/auth/success").failureForwardUrl("/auth/failed"); //登录成功/失败跳转的页面
    }

}

修改登录controller

@Controller
@RequestMapping("/auth")
public class LoginController {
    
    //登录成功失败界面 
    @GetMapping("/success")
    public String success(){
        return "success";
    }
    @PostMapping("/failed")
    public String failed(){
        return "failed";
    }

    //首页
    @RequestMapping("/index")
    public String index(){
        return "index";
    }


    //获取当前用户
    @PostMapping("/getCurrUser")
    @ResponseBody
    public Object getCurrentUser(HttpSession session){
        return session.getAttribute("current_user");
    }

    //登出
    @PostMapping("logout")
    @ResponseBody
    public ResponseEntity logout(HttpSession session){
        session.removeAttribute("current_user");
        return ResponseEntity.ok("已退出登录");
    }

    /**
     * 获取当前用户的多种方式
     * @param principal
     * @return
     */
    @GetMapping("/getUserByPrincipal")
    public String getUserByPrincipal(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();
//    }

}

将首页/登录成功页/失败页复制过来

修改首页中获取当前用户部分

   function currentUser() {
       $.ajax({
           type : "GET",
           url : "/auth/getUserByPrincipal",
           data : {},
           success : function (result) {
               result = JSON.stringify(result);
               $('#currentUser').html("success:" + result);
           },
           error : function (error) {
               $('#currentUser').html("error:" + error);
           }
       })
   }

至此简单的spring security项目准备完毕,但是此时用户信息及权限规则,均通过UserDetailsService写至内存中,实际项目需要结合数据库来获取用户权限数据

测试

启动项目-》登录前,直接请求/banana接口,发现均会跳转至自带登录界面,用banana/banana登录后,正常获取banana资源,而获取不到apple资源

 直接访问http://127.0.0.1:5556/banana

 正常获取数据,访问http://127.0.0.1:5556/apple

返回403无法访问

使用自带/logout接口退出当前用户后,访问/banana接口,则此时跳转至登录界面

Spring Security拓展点

数据源

SpringSecurity通过引用Spring容器的UserDetailsService来管理用户数据,默认情况下会提供一个为user的用户,可通过自定义注入改变用户数据,但实际开发中,用户数据及权限配置信息来源于数据库。在SpringSecurity提供JdbcUserDetailsManager对数据库的用户数据进行管理

自定义登录界面整合数据库权限管理

修改配置从数据库获取用户及权限

修改UserDetails,从数据库加载用户信息

修改WebSecurityConfig中httpSecurity,改成从数据库加载授权配置

密码解析器

SpringSecurity提供多种密码解析器,CryptPassEncoder/Argon2PasswordEncoder等,接实现PassEncoder接口,最常见为BCryptPasswordEncoder

自定义授权等配置

自定义拦截配置

配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,通过重写的configure(HttpSecurity httpSecurity)进行拦截规则配置,包括访问控制,登录登出界面设置等

自定义登录

通过配置HttpSecurity中的方法来自定义

loginPage()配置登录页面

如在WebSecurityConfig中新增.loginPage("/auth/index")

当输入http://127.0.0.1:5556/login

页面跳转为自定义界面 http://127.0.0.1:5556/auth/index

此时需要修改登录界面index.html 及登录请求接口的逻辑

修改index.html中action 编写/auth/tologin接口用于处理登录逻辑,自定义后,将不会使用UserDetailsService来进行处理

<form action="/auth/tologin" method="post">
    用户名:<input type="text" name="userName" required><br/>
    密 码:<input type="text" name="password" required><br/>
    <input type="submit" value="登录"> <input type="reset" value="重置">
</form>

loginProcessingUrl() 设置登录逻辑

remember-me

登录时提交remember-me参数 on/yes/1/true 则会记住当前用户的token至cookie中(默认失效时间2周.tokenValiditySeconds(60)设置失效时间)

httpSecurity.rememberMe().rememberMeParameter("remember-me") //登录时,前端传参的名称

安全拦截

httpSecurity..authorizeRequests().antMachers()设置请求路径匹配

antMachers().permitAll()允许所有人访问,

antMachers().denyAll()所有人拒绝访问,

antMachers().anonymous()未登录可以访问,已登录不可访问

antMachers().hasAuthority()/hasRole() 配置需要有对应的权限/角色才能访问

跨域配置csrf

Cross-Site Request Forgery 跨站点请求伪造,一种安全攻击手段,利用客户端信息伪装成正常用户进行攻击。

当打开csrf配置后,SpringSecurity提供CsrfFilter对csrf参数进行检查,每次请求,session中加入_csrf的token,每次带上token,SpringSecurity进行检查

异常处理

@ControllerAdvice注入一个异常处

理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。

结合注解的使用

权限控制注解

以下四个注解默认不生效,需要在配置类(MyWebAppConfigurer)或者启动类加上@EnableGlobalMethodSecurity(prePostEnabled = true)开启

@PreAuthorize("hashRole(‘admin’)") 执行方法前判断是都具有该角色权限

注解支持ROLE_admin/admin均可,但是配置类中.antMatchers("/test/**").hasRole("stu")不能带上"ROLE_"

@PostAuthorize("returnObject.name == authentication.name") 方法执行后,判断返回的值是否与认证主体中的某个值相等,若相等则返回,否则抛出异常

@PreFilter(filterTarget=”a“, value=”filterObject%2==0“) 方法执行前,过滤参数a 当a%2不等于0则会被过滤,不会传入方法中(入参a 为List<Integer> a)

@PostFilter("filterObject.name == authentication.name“) 方法执行后根据条件过滤结果

角色控制注解

@EnableGlobalMethodSecurity(securedEnable = true)开启。

@Secured("ROLE_stu") 作用于方法,类,判断是否具备该角色(角色严格区分大小写)

会话控制

获取当前用户

通过认证后,SpringSecurity提供会话管理,保存用户的认证信息,以避免每次操作都要进行认证操作。认证通过后,身份信息将放入SecurityContextHolder上下文,SecurityContext与当前线程绑定,用于方便获取用户身份

通过SecurityContextHolder.getContext().getAuthentication()获取当前用户登录信息

 /**
     * 获取当前用户的多种方式
     * @param principal
     * @return
     */
    @GetMapping("/getUserByPrincipal")
    @ResponseBody
    public String getUserByPrincipal(Principal principal){
        return principal.getName();
    }
    
    @GetMapping(value = "/getLoginUserByAuthentication")
    @ResponseBody
    public String currentUserName(Authentication authentication) {
        return authentication.getName();
    }
    
    @GetMapping(value = "/username")
    @ResponseBody
    public String currentUserNameSimple(HttpServletRequest request) {
        Principal principal = request.getUserPrincipal(); return principal.getName();
    }
    
    @GetMapping(value = "/username")
    @ResponseBody
    public String getUserByContext() {
        Principal principal = (Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return principal.getName();
    }

 会话控制

 通过配置sessionCreationPolicy参数来管理Session

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //定义安全拦截策略
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
......

 策略包含

always 如果无Session则创建一个

if required 如果需要登录时创建一个(默认)

never SpringSecurity不会创建Session,如果应用其他地方创建,则会被使用

stateless SpringSecurity绝不创建也不使用Session

会话超时

在yml文件中直接设置session过期时间

server.servlet.session.timeout = 3000s (springsecurity默认一分钟 小于一分钟也会按照一分钟失效处理)

session过期后可通过SpringSecurity设置跳转界面

httpSecurity . sessionManagement ()
    . expiredUrl ( "/auth/index ?error=EXPIRED_SESSION" ) //session过期后跳转
    . invalidSessionUrl ( "/auth/index ?error=INVALID_SESSION" ); //传入无效sessionId跳转

 同账号多端登录前者被踢下线功能

httpSecurity.sessionManagement()

    .maximumSessions(1)  //设置某段时间内允许几个端登录

    .maxSessionsPreventsLogin(false) //true表示已经登录则不允许再次登录 false允许再次登录但前者会被踢下线

     .expiredSessionStrategy(new CustomExpiredSessionStrategy()) //自定义被踢下线后的操作 需要实现自定义策略

public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy{

    private ObjectMapper  objectMapper = new ObjectMapper();

    @override

    public void onExpiredSessionDetected(SessionInformationExpiredEvent event){

       Map<String, Object> map = new HashMap();

       map.put("code",403);

       map.put("msg","您已在其他地方登录,被迫下线" + event.getSessionInformation().getLsatRequest());

       String json = objectMapper.writeValueAsString(map);

       event.getResponse().setContentType("application/json;charset=utf-8");

       event.getResponse().getWriter().write(json);

    }

}

通过两个不同的浏览器进行同账号登录测试

安全会话cookie

在yml文件中直接设置

server.servlet.session.cookie.http only = true # true 浏览器脚本无法访问cookie
server.servlet.session.cookie.secure = true # true cookie仅通过Https发送

退出登录

SpringSecurtity提供退出接口/logout 跳转至登出界面,可以直接调用

也可在WebSecurityConfig中自定义退出界面/退出后跳转地址

.and() 
.logout() //开启自定义退出,使用WebSecurityConfigurerAdapter 会自动被应用 .logoutUrl("/logout") //默认退出地址 
.logoutSuccessUrl("/auth/logout") //退出后的跳转地址 
.addLogoutHandler(new SecurityContextLogoutHandler()) //添加LogoutHandler,负责退出时的清理工作.默认 SecurityContextLogoutHandler会被添加为最后一个LogoutHandler 
.invalidateHttpSession(true); //指定是否在退出时让HttpSession失效,默认是true

退出时,会进行的操作(SecurityContextLogoutHandler)

1.http session失效

2.清除SecurityContext上下文

3.跳转至退出后的地址

SecurityContextLogoutHandler退出时负责SecurityContext的清理工作

分布式系统的认证方案

单体系统演变至多服务,多个服务利用一套独立的第三方系统提供统一的认证授权

统一认证授权

独立的认证服务,用于统一处理认证授权,不管是不同类型的用户,不同类型的客户端(web,h5,小程序,app等),采用一致的认证授权会话处理机制,实现统一登录认证授权

需要支持多种认证方式:账号密码,短信验证,二维码,人脸识别等灵活切换

多重认证场景

购物,支付需要不同安全等级需要对应认证场景

应用接入认证

开放部分API给第三方使用。内部与外部第三方采用统一的

认证方案(基于session与token两种方案)

方案1基于session的认证方式

在分布式中,将session信息同步至各个服务,并对请求进行负载均衡

做法:

1.进行session复制:多个服务器之间同步session,使得session保持一致,但是对外透明

2.session黏贴:用户访问服务器集群中某台服务器后,后续所有请求都需要落到该服务器上

3.session集中存储:将session存入分布式缓存中,所有服务器均从分布式缓存中获取session信息

优点:更好的在服务端进行会话控制,安全性较高

缺点:但是客户端需要存储sessionId,对不同的客户端不能有效使用等

方案2基于token的认证方式

生成认证的token存储,每次请求携带并进行校验

选择方案

统一认证服务(UAA)和网关结合来进行认证授权

uaa负责接入方认证,登录用户认证,授权,令牌管理,完成实际的用户认证授权

API网关作为整个分布式系统的统一入口,进行身份认证,监控,负载均衡等


OAuth2.0

前言

OAuth开放授权标准,允许用户授权第三方应用B获取在微信上注册的信息,从而不需要向B提供微信账号密码,避免B获取用户在微信上的所有数据内容。OAuth协议用于保证双方的可信。

认证流程

用户在B上选择通过微信登录-》弹出微信登录方式(账号密码/二维码等)-》选择弹出的微信同意登录-》B获取用户微信账号信息-》B新建账号与微信绑定-》用B账号进行登录

一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求

OAuth2中的角色

客户端Client:如浏览器,微信客户端,不存储资源,需要通过资源拥有者授权去请求资源服务器的资源

资源拥有者Resource Owner:用户,也可以是应用程序,表示某个资源的拥有者

授权服务器Authorization Server:认证服务器,如微信,服务提供者对资管拥有者的身份认证,对访问资源进行授权,认证成功给客户端发放令牌(access_token)作为凭证

资源服务器Resource Server:如微信服务器,B服务器,通过OAuth协议让B获取用户在微信上的用户信息,B同时通过OAuth协议让用户访问自己的资源

clientDetails:cilent_id客户信息,代表B在微信中的唯一索引
secret :密钥,B获取微信的信息需要提供加密字段
scope:授权作用域,代表B获取微信的信息访问
access_token:授权码,B获取微信用户信息的凭证,如微信的接口调用凭证
grant_type:授权类型,如微信支持基于授权码的authorization_code模式,OAuth提供多种授权方式
userDetails:授权用户标识user_id,如微用户在微信中微信号

Spring Security OAuth2.0

基于OAuth2协议的服务实现框架,OAuth2包含 授权(认证)服务资源服务,可在同一个应用中实现两个服务或者拆分多个应用实现。

授权服务(Authorization Server)

包含接入端及用户登入的合法性验证并发放token等,配置一个认证服务必须具有的endpoints
AuthorizationEndpoint 服务用于认证请求。默认URL:/oauth/authorize
TokenEndpoint 服务用于访问令牌的请求。默认URL:/oauth/token
OAuth2AuthenticationProcessingFilter 用于对请求方给出的身份令牌进行解析鉴权

认证流程

1.客户请求server-uaa服务申请access_token授权码

2.客户携带access_token访问server-b服务

3.server-b校验access_token合法性,若合法返回资源信息

OAuth2简单运用实例(以码云为资源/认证服务器)

描述:通过码云实现模拟第三方登录(授权码模式)

客户端及第三方应用创建

1.新建简单项目,创建个简单回调页面(客户端)

2.在码云上注册应用,获取Client ID(客户端id),Client Secret(密钥)等(授权服务器,码云同为资源服务器)

上面说过一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求,所以此处创建第三应用可以理解为,在码云上登记一个客户端信息

3.点击模拟请求进行测试-》同意授权

4.页面跳转至我们设置的回调界面,在链接上带回一个code

 5.查看码云OAuth文档

Gitee OAuth 文档https://gitee.com/api/v5/oauth_doc

通过授权码模式的认证授权

请求的几个参数

1.clientDetails(client_id) 客户信息,第三方应用id,项目在码云中的唯一索引

2.secret 密钥,代表获取码云信息需要提供的加密字段(与码云加密算法有关)

3.scope 授权作用域,可以获取码云信息的范围

4.access_token 授权码,允许获取码云信息的凭证

5.grant_type 四种授权类型

1.通过请求,进行授权模拟(授权码模式)

.根据文档,通过浏览器将用户引导至码云第三方认证页面(GET)

https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code

 带入实际参数client_id,redirect_uri,response_type

https://gitee.com/oauth/authorize?client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&response_type=code

2.出现第3步,第四步相同操作

3. 将授权码code向码云认证服务器发送POST请求,获取access_token(有效期一天)

https://gitee.com/oauth/token?grant_type=authorization_code&code=c59e19acf5eb78211ca4246d21cd652cbba0e3124d86c7d9ea4deeecd1e9d513&client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&client_secret=9548b96604a5ab1a50b201c09065a2e0c84e95be3c4163704ee29341080afcd5

当access_token失效后,可以通过refresh_token重新获取access_token(POST)

https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}

 OAuth2协议包含的角色

1.客户端:例子中创建的客户端服务,本身无资源,通过浏览器去获取码云资源,及需要通过资源拥有者的授权去资源服务器获取资源

2.资源拥有者,例子中拥有码云账号的用户,资源拥有者

3.授权服务器(认证服务器),例子中码云认证服务器,对资源拥有者身份认证授权,认证成功发放令牌(access_token)作为客户端访问资源服务器凭证

4.资源服务器,例子中码云,存储资源的云服务器,通过OAuth协议,用户可以获取码云账户数据,仓库数据,代码等资源

OAuth2协议的四种授权类型(grant_type)

授权码模式authorization-code

用户访问客户端,客户端向授权服务器发起授权

授权服务器,引导用户进入授权界面,等待用户同意授权

用户同意授权

回调客户端界面,带回授权码

客户端通过授权码向授权服务器请求获取令牌access_token

适用:安全性最高,最常用的流程

隐藏式implicit

授权码模式简化

与授权码模式相似,但是用户同意授权后直接返回access_token

适用:纯前端项目

密码模式password

客户端与资源所有者高度可信情况下可用,如客户端为系统的一部分

资源拥有者直接提供账号密码给客户端,向授权服务器申请令牌access_token

适用:高度信任的情况下

客户端模式client credentials

该情况适用于无前端的纯后台项目,客户端向授权服务器发送身份信息,请求access_token

适用:纯后端项目

OAuth2简单运用实例(SpringBoot简单整合)

step1基础配置

本例资源服务,认证服务均为同一个服务,采用客户端模式(client_credentials)

1.yml文件

server:
  port: 8888

spring:
  application:
    name: oauth2-demo

2.pom文件依赖

引入安全认证依赖,引入后,springboot便会对资源服务器上所有资源进行默认的保护

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

step2认证服务器配置(包括3部分)

1.token endpoint安全束缚配置

主要配置允许客户端以Form表单形式登录/配置密码加密方式等)

2.客户端详情设置

客户端详情包括(client_id,client_secret,grant_type,scope)

剋设置客户端详情存储位置,内存或者数据库

3.配置授权,token endpoint,令牌服务

认证服务简单配置

本例采用客户端模式

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    //配置采用的加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    //配置安全约束 定义令牌终结点上的安全约束
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许客户端以form表单登录,如微信获取access token
        security.allowFormAuthenticationForClients();
    }

    //配置客户端详细信息
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //client_id
                .withClient("client")
                //授权方式 "authorization_code", "password", "client_credentials", "implicit", "refresh_token"
                .authorizedGrantTypes("client_credentials")
                // 授权范围 all表示所有,write等
                .scopes("all")
                // client_secret配置加密类型,如果不需要 则直接 .secret("{noop}123456"); {noop表示空操作}
                .secret(new BCryptPasswordEncoder().encode("123456"));

    }

    //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        super.configure(endpoints);
    }

}

step3资源服务配置(2个部分)

1.资源服务器安全配置

2.http安全配置,保护资源API

配置类

@Configuration
@EnableResourceServer
public class MyResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        super.configure(resources);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/apple/**").authenticated();
    }
}

资源请求API

@RestController
@RequestMapping("/apple")
public class AppleController {
    @GetMapping
    public String getApple(){
        return "资源1苹果";
    }
}

至此简单的认证服务与资源服务配置完成,本例认证服务与资源服务为同一个项目

未授权进行资源的请求,此时报错,提示未授权

 获取token

SpringBoot OAuth 默认获取token的endpoint路劲为 /oauth/token

 其中expires_in 为失效时间(秒),每次重复请求,返回同一个token,但是失效时间再减少

 带上token进行请求,得到结果

客户端模式整合数据库

目的:将客户端Client信息与token存储至数据库(token一般存放redis)

本例中用到postgre sql

简单创建需要的表

create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY, -- 客户端id
  resource_ids VARCHAR(256) ,  -- 资源id集合,英文逗号隔开
  client_secret VARCHAR(256) ,  -- 客户端密钥
  scope VARCHAR(256) , -- 授权范围
  authorized_grant_types VARCHAR(256)  , -- 授权类型 authorization_code,password,refresh_token,implicit,client_credentials,英文逗号隔开
  web_server_redirect_uri VARCHAR(256), --  客户端重定向uri
  authorities VARCHAR(256), --  客户端拥有spring security权限值
  access_token_validity INTEGER , -- token有效时间 秒 默认12小时
  refresh_token_validity INTEGER , -- refresh_token有效时间 默认30天
  additional_information VARCHAR(4096) , -- 预留json字段
  autoapprove VARCHAR(256) -- 是否启动自动approve操作,适用于授权码模式authorization_code
);

create table oauth_access_token (
  token_id VARCHAR(256) , -- MD5算法加密后的access_token
  token bytea, -- access_token序列化二进制数据格式 mysql中为BLOB类型
  authentication_id VARCHAR(256) PRIMARY KEY , -- 根据username,client_id,scopeMD5加密生成的主键
  user_name VARCHAR(256),  -- 用户名
  client_id VARCHAR(256),  -- 客户端id
  authentication bytea, --  OAuth2Authentication对象序列化后的二进制数
  refresh_token VARCHAR(256) -- refresh_token MD5加密后的值
);

官方完整建表sql

https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

创建客户端数据

注意此处密码应该进行BCrypt加密

123456 对应

$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu

INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('client', 'apple', '$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu', 'all', 'client_credentials', null, null, null, null, null, null);

pom文件新增对应数据库的支持依赖

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

yml

server:
  port: 8888

spring:
  application:
    name: oauth2-demo
  datasource:
    # 数据库引擎
    driver-class-name: org.postgresql.Driver
    # 数据库地址 characterEncoding防止出现中文乱码 若为https则无需加useSSL
    url: jdbc:postgresql://127.0.0.1:5432/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&yearIsDateType=false&stringtype=unspecified
    # 用户
    username: postgres
    # 密码
    password: 123456
    # 数据库连接类型
    # 时区
    jackson:
      time-zone: GMT+8

资源服务修改

给资源配置resourceId

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception 
    //super.configure(resources);
    resources.resourceId("apple");
}

认证服务配置修改

从原来从内存中获取客户端信息改为从数据库获得客户端信息

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    
    private final DataSource dataSource;
    
    public MyAuthorizationConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    //配置采用的加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }
    
    //配置安全约束 定义令牌终结点上的安全约束
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许客户端以form表单登录,如微信获取access token
        security.allowFormAuthenticationForClients();
    }
    
    //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new JdbcTokenStore(dataSource));
    }
}

请求测试

每次请求,OAuth会去客户端信息表oauth_client_details查询当前发送过来的客户信息是否正确,认证成功后将返回令牌access_token,同时由于设置了将令牌存储至数据库,此时oauth_access_token也会新增一条数据

postman认证成功返回令牌 

 

成功获取资源服务的资源

 至此OAuth2 客户端授权模式结合数据库存储的简单例子结束,本例中创建了两张需要的表,一为客户端详情表,代替之前在代码中将客户端信息写入内存的方式,二是令牌token表,用于存储token,但实际开发中,token一般不存储在数据库中,不然每次请求接口都需要访问数据库

access_token的存储方案

上面的例子已经设计将token存在内存,数据库中,但更合理的应该存在redis上,且redis可设置数据的有效期。这样可以避免高并发情况下频繁访问数据库。在分布式架构中,将token存在其中一台服务器实例的内存中也不合适。

将token存储至redis

pom新增redis依赖

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

yml配置文件新增redis配置

redis:
  # Redis数据库索引(默认为0)
  database: 0
  # Redis服务器地址 本地localhost/127.0.0.1
  host: 127.0.0.1
  # Redis服务器连接端口
  port: 6379
  # 连接超时时长(毫秒)
  timeout: 6000
  # Redis服务器连接密码(默认为空)
  password:

认证服务MyAuthorizationConfig修改

令牌访问端点修改为redis存储方式

@Resource
private RedisConnectionFactory redisConnectionFactory;

......

//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory));
    //endpoints.tokenStore(new JdbcTokenStore(dataSource));
}

启动本地redis,启动服务请求获得token,此时redis中

通过JWT充当令牌

JWT简单介绍

JSON Web Token(JWT),客户端服务器通过JWT规定格式进行身份认证完成交互,JWT分三段,每段通过.隔开,包含不同信息分别为

Header头部:JSON数据Base64编码,包含加密类型等

PayLoad负载:JSON数据Base64编码,包含客户端授权信息

Signature签名:头部+负载+密钥(盐)通过加密算法获得,防止数据篡改(盐存在服务端,保密)

导入依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.1.1.RELEASE</version>
</dependency>

修改认证服务配置

//设置密钥串
private static final String SIGNING_KEY = "oauth_test";

......

//令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
    //设置token转换器
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    //设置加签密钥
    jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
    //设置校验器
    jwtAccessTokenConverter.setVerifier(new MacSigner(SIGNING_KEY));
    TokenStore tokenStore = new JwtTokenStore(jwtAccessTokenConverter);
    endpoints.accessTokenConverter(jwtAccessTokenConverter);
    endpoints.tokenStore(tokenStore);
}

 启动服务请求获得token,并成功请求资源接口

 此token通过官网解码JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).https://jwt.io/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值