• 基于Session的认证与授权实践


    Spring Security系列文章

    • 认证与授权之Cookie、Session、Token、JWT
    • 基于Session的认证与授权实践

    基于Session的认证方式

    基于 session 的认证方式如下图:

    基于 session 的认证方式

    基于 Session 的认证机制由 Servlet 规范定制,Servlet 容器已实现,用户通过 HttpSession 的操作方法即可实现,如下是 HttpSession 相关的操作API。

    HttpSession核心方法

    创建工程

    本项目使用 maven 搭建,使用 SpringMVC、Servlet3.0 实现。

    创建maven工程

    1、导入依赖

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <parent>
        <artifactId>spring-security-studyartifactId>
        <groupId>com.msdn.securitygroupId>
        <version>1.0-SNAPSHOTversion>
      parent>
      <modelVersion>4.0.0modelVersion>
    
      <artifactId>springmvc-sessionartifactId>
    
      <packaging>warpackaging>
      <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
      properties>
    
    
      <dependencies>
        <dependency>
          <groupId>org.springframeworkgroupId>
          <artifactId>spring-webmvcartifactId>
          <version>5.3.23version>
        dependency>
        <dependency>
          <groupId>javax.servletgroupId>
          <artifactId>javax.servlet-apiartifactId>
          <version>4.0.1version>
          <scope>providedscope>
        dependency>
        <dependency>
          <groupId>org.projectlombokgroupId>
          <artifactId>lombokartifactId>
          <version>1.18.20version>
        dependency>
        <dependency>
          <groupId>cn.hutoolgroupId>
          <artifactId>hutool-allartifactId>
          <version>5.8.5version>
        dependency>
      dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.pluginsgroupId>
            <artifactId>maven-compiler-pluginartifactId>
            <configuration>
              <source>8source>
              <target>8target>
            configuration>
          plugin>
        plugins>
      build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    Servlet Context配置

    本案例采用 Servlet3.0 无 web.xml 方式,在 config 包下定义 WebConfig.java,它对应于 DispatcherServlet 配置。

    @Configuration//就相当于springmvc.xml文件
    @EnableWebMvc
    @ComponentScan(basePackages = "com.msdn.security"
            ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
    public class WebConfig implements WebMvcConfigurer {
    
        //视图解析器
        @Bean
        public InternalResourceViewResolver viewResolver(){
            InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
            viewResolver.setPrefix("/WEB-INF/view/");
            viewResolver.setSuffix(".jsp");
            return viewResolver;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    加载Spring容器

    在 init 包下定义 Spring 容器初始化类 SpringApplicationInitializer,此类实现 WebApplicationInitializer 接口,Spring 容器启动时加载WebApplicationInitializer 接口的所有实现类。

    public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return null;
        }
    
        //servletContext,相当于加载springmvc.xml
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{WebConfig.class};
        }
    
        //url-mapping
        @Override
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    SpringApplicationInitializer 相当于 web.xml,使用了servlet3.0开发则不需要再定义 web.xml,WebConfig.class 对应以下配置的 spring-mvc.xml,web.xml的内容参考:

    <web‐app>
        <listener>
            <listener‐class>org.springframework.web.context.ContextLoaderListenerlistener‐class>
        listener>
        <context‐param>
            <param‐name>contextConfigLocationparam‐name>
            <param‐value>/WEB‐INF/application‐context.xmlparam‐value>
        context‐param>
      
        <servlet>
            <servlet‐name>springmvcservlet‐name>
            <servlet‐class>org.springframework.web.servlet.DispatcherServletservlet‐class>
            <init‐param>
                <param‐name>contextConfigLocationparam‐name>
                <param‐value>/WEB‐INF/spring‐mvc.xmlparam‐value>
            init‐param>
            <load‐on‐startup>1load‐on‐startup>
        servlet>
        <servlet‐mapping>
            <servlet‐name>springmvcservlet‐name>
            <url‐pattern>/url‐pattern>
        servlet‐mapping>
     
    web‐app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    实现认证功能

    认证页面

    在 webapp/WEB-INF/view 下定义认证页面 login.jsp,本案例只是测试认证流程,页面没有添加css样式,页面实现可填入用户名,密码,触发登录将提交表单信息至/login,内容如下:

    <%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
    
    
        用户登录
    
    
    

    ${msg }

    用户名:
    密   码:
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在 WebConfig 中新增如下配置,将/直接导向 login.jsp 页面:

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
      registry.addViewController("/").setViewName("login");
    }
    
    • 1
    • 2
    • 3
    • 4

    启动项目,配置 tomcat

    SprignMVC项目配置tomcat

    SprignMVC项目配置tomcat

    认证接口

    用户进入认证页面,输入账号和密码,点击登录,请求/login 进行身份认证。

    1、定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:

    public interface AuthenticationService {
    
      /**
       * 用户认证
       *
       * @param userRequest
       * @return
       */
      User authentication(UserRequest userRequest);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2、表单请求参数封装为实体类

    @Data
    public class UserRequest {
    
      private String username;
      private String password;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3、认证成功后返回的用户详细信息

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
    
      private Long id;
      private String username;
      private String password;
      private String fullname;
      private String mobile;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4、认证服务具体实现类

    @Service
    public class AuthenticationServiceImpl implements AuthenticationService {
    
      private static Map<String, User> userMap = new HashMap<>();
    
      static {
        userMap.put("zhangsan", new UserDTO(1010L, "zhangsan", "123", "张三", "133443"));
        userMap.put("lisi", new UserDTO(1011L, "lisi", "456", "李四", "144553"));
      }
    
      @Override
      public User authentication(UserRequest userRequest) {
        User user = getUserByName(userRequest.getUsername());
        if (Objects.isNull(user)) {
          throw new RuntimeException("查询不到该用户");
        }
    
        if (!Objects.equals(user.getPassword(), userRequest.getPassword())) {
          throw new RuntimeException("账号或密码错误");
        }
    
        return user;
      }
    
      /**
         * 模仿从表中根据用户名查询用户信息
         *
         * @param username
         * @return
         */
      public User getUserByName(String username) {
        return userMap.get(username);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    5、controller 对 login 请求做处理

    @RestController
    public class LoginController {
    
      @Autowired
      private AuthenticationService authenticationService;
    
      @PostMapping(value = "/login")
      public String login(UserRequest request, Model model) {
        if (Objects.isNull(request) || isBlank(request.getUsername()) ||
            isBlank(request.getPassword())) {
          model.addAttribute("msg", "账号或密码为空");
          return "login";
        }
        try {
          User user = authenticationService.authentication(request);
    
          return "redirect:hello";
        } catch (Exception e) {
          model.addAttribute("msg", e.getMessage());
        }
        return "login";
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    6、测试,重新启动 tomcat

    输入正确的用户名和密码,则提示登录成功,如果账号或密码不输入,则会提示报错信息;如果账号或密码校验不通过,会提示具体报错。

    实现会话功能

    会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。

    认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性拦截。因此,在认证成功后,一般会把认证成功的用户信息放入 Session中,在后续的请求中,系统能够从 Session 中获取到当前用户,用这样的方式来实现会话机制。

    在上一节我们详细介绍了 Cookie 和 Session,我们此处创建的项目启动后就作为临时服务器,存储 session 信息,而客户端通常是将 sessionId 存放在 cookie 中的,所以我们还需要设置 cookie 返回给客户端。

    1、cookie 操作工具类

    public class CookieUtil {
    
    
      public static Cookie addUserCookie(String cookieValue) {
        return addCookie("user_session_id", cookieValue);
      }
    
      public static Cookie addCookie(String cookieName, String cookieValue) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(3600);
        cookie.setPath("/");
        return cookie;
      }
    
      public static String getUserCookie(HttpServletRequest request) {
        return getCookie(request, "user_session_id");
      }
    
      public static String getCookie(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        String cookieValue = "";
        for (Cookie cookie : cookies) {
          if (cookieName.equals(cookie.getName())) {
            cookieValue = cookie.getValue();
          }
        }
        return cookieValue;
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2、修改 controller 中 login 方法,当认证成功后,将用户信息放入当前会话,并将 sessionId 放入 cookie 中。并增加用户登出方法,登出时将 session 置为失效。

    @PostMapping(value = "/login")
    public String login(UserRequest request, HttpSession session, Model model,
                        HttpServletResponse response) {
      if (Objects.isNull(request) || isBlank(request.getUsername()) ||
          isBlank(request.getPassword())) {
        model.addAttribute("msg", "账号或密码为空");
        return "login";
      }
      try {
        User user = authenticationService.authentication(request);
        String userSessionId = RandomUtil.getRandom().nextInt(10000) + "_user";
        session.setAttribute(userSessionId, user);
        Cookie cookie = CookieUtil.addUserCookie(userSessionId);
        response.addCookie(cookie);
    
        return "redirect:hello";
      } catch (Exception e) {
        model.addAttribute("msg", e.getMessage());
      }
      return "login";
    }
    
    
    @RequestMapping(value = "logout")
    public String logout(HttpSession session) {
      session.invalidate();
      return "login";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3、在 controller 中增加资源访问测试接口,判断 session 中是否有用户

      @RequestMapping(value = "/r/r1")
      public String r1(HttpServletRequest request, Model model) {
        String userSessionId = CookieUtil.getUserCookie(request);
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute(userSessionId);
        String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
        model.addAttribute("text", fullName + " 访问资源1");
    
        return "resource";
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4、重启 tomcat,未登录情况下直接访问测试资源 r/r1,具体路径为:http://localhost:8080/r/r1

    实现授权功能

    现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:

    • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
    • 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。

    1、增加权限数据

    实际工作中,用户和角色关联,然后角色又和权限表关联,在本次测试阶段,为了方便操作,我们直接在 User 里增加权限属性。

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
    
      private Long id;
      private String username;
      private String password;
      private String fullname;
      private String mobile;
    
      // 用户权限
      private Set<String> authorities;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2、并在 AuthenticationServiceImpl 认证服务具体实现类中给用户初始化权限,实际应用中肯定不会这样,会从数据库中获取用户信息。

      private static Map<String, User> userMap = new HashMap<>();
    
      static {
        Set<String> authoritie1 = new HashSet<>();
        authoritie1.add("p1");
        Set<String> authoritie2 = new HashSet<>();
        authoritie2.add("p2");
        userMap.put("zhangsan", new User(1010L, "zhangsan", "123", "张三", "133443", authoritie1));
        userMap.put("lisi", new User(1011L, "lisi", "456", "李四", "144553", authoritie2));
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3、增加测试资源

    在 controller 文件中增加对资源 r1、r2 的访问

    @RequestMapping(value = "/r/r1")
    public String r1(HttpServletRequest request, Model model) {
      String userSessionId = CookieUtil.getUserCookie(request);
      HttpSession session = request.getSession();
      User user = (User) session.getAttribute(userSessionId);
      String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
      model.addAttribute("text", fullName + " 访问资源1");
    
      return "resource";
    }
    
    @RequestMapping(value = "/r/r2")
    public String r2(HttpServletRequest request, Model model) {
      String userSessionId = CookieUtil.getUserCookie(request);
      HttpSession session = request.getSession();
      User user = (User) session.getAttribute(userSessionId);
      String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
      model.addAttribute("text", fullName + " 访问资源2");
    
      return "resource";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4、实现授权拦截器

    在 interceptor 包下定义 SimpleAuthenticationInterceptor 拦截器,实现授权拦截:

    1. 校验用户是否登录
    2. 校验用户是否拥有操作权限
    @Component
    public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
    
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
          throws Exception {
        String userSessionId = CookieUtil.getUserCookie(request);
        Object attribute = request.getSession().getAttribute(userSessionId);
        if (Objects.isNull(attribute)) {
          writeContent(response, "请先登录");
        }
        User user = (User) attribute;
        String requestURI = request.getRequestURI();
        if (user.getAuthorities().contains("p1") && requestURI.contains("r1")) {
          return true;
        }
        if (user.getAuthorities().contains("p2") && requestURI.contains("r2")) {
          return true;
        }
        if (requestURI.contains("resource")) {
          return true;
        }
    
        writeContent(response, "权限不足,无法访问");
        return false;
      }
    
      private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.print(msg);
        writer.close();
        response.resetBuffer();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在 WebConfig 中配置拦截器,匹配 /r/**的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor 拦截器。

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4、重启 tomcat,张三登录后,分别访问 r1 和 r2 资源,查看页面返回信息。

    项目演示

    1、登录

    如果账号或密码为空,点击登录按钮,则会提示“账号或密码为空”。

    登录时账号或密码为空

    如果账号或密码错误,点击登录按钮,页面展示如下:

    登录时账号或密码错误

    如果账号和密码都正确,点击登录按钮,页面展示如下:

    登录成功页面展示

    登录成功后,我们在浏览器上查看 cookie 中存储的 sessionId。

    浏览器查看sessionId

    2、资源访问

    张三可以访问 r1 资源,但无权访问 r2 资源。

    有权访问资源r1

    无权访问资源r1

    李四可以访问 r2 资源,但无权访问 r1 资源。

    小结

    基于 session 的认证和授权方式比较简单,认证过程清晰明了,但是在大型项目中修改麻烦,不易扩展。所以实际生产中我们往往会考虑使用第三方安全框架(如 Spring Security,shiro等安全框架)来实现认证授权功能。

    本文主要还是对上一篇文章中提到的知识点进行实操,方便大家直观理解,关于登录认证还有其他操作,比如记住密码等,这里就不过多介绍了。

  • 相关阅读:
    程序员想要网上接单却不知道如何是好?那这篇文章你可得收藏好了!
    AbstractApplicationContext抽象类解读
    Linux 查看 CPU核数 及 内存
    GPT-4o:引领人工智能新时代的强大引擎
    目标检测 YOLOv5 - YOLOv5的后处理
    Python文件操作(03):写文件
    【JAVA学习笔记】43 - 枚举类
    赵运泓:12:5黄金行情走势分析
    什么是域名?如何注册域名?
    记录一次通过社工进网站后台
  • 原文地址:https://blog.csdn.net/Herishwater/article/details/128081055