美文网首页
基于Session的认证方式

基于Session的认证方式

作者: Doooook | 来源:发表于2020-12-07 22:29 被阅读0次

    1. 认证流程

    基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session中(当前会话)中,发给客户端对应的session_id存放到cookie中,这样用户请求时带上session_id就可以验证服务器端是否存在session数据,以完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

    基于session的认证方式

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

    HttpSession相关的操作API

    2. 创建工程

    本案例工程使用maven进行构建,使用SpringMVC、Servlet3.0实现。

    2.1 创建maven工程

    创建maven工程security-springmvc,工程结构如下:

    工程结构

    2.2 Spring容器配置

    在config包下定义ApplicationConfig,它对应web.xml中的ContextLoaderListener

    /**
     * 相当于apllicationContext.xml配置文件
     */
    @Configuration
    @ComponentScan(basePackages = "com.pengjs.book.admin.security",
            excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
    public class ApplicationConfig {
        // 配置除了Controller的其他bean,如:数据库连接池、事务管理器、业务bean等
    
    }
    

    2.3 ServletContext配置

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

    /**
     * 相当于springmvc.xml配置文件
     */
    @Configuration
    @EnableWebMvc
    @ComponentScan(basePackages = "com.pengjs.book.admin.security",
            includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
    public class WebConfig implements WebMvcConfigurer {
    
        @Autowired
        private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
    
        /**
         * 配置视图解析器
         * @return
         */
        @Bean
        public InternalResourceViewResolver viewResolver() {
            InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
            viewResolver.setPrefix("/WEB-INF/view/");
            viewResolver.setSuffix(".jsp");
            return viewResolver;
        }
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            // 将"/"定向到"/login"
            registry.addViewController("/").setViewName("login");
        }
    
        /**
         * 添加拦截器,让拦截器生效
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // login接口不拦截,只拦截:/r/**
            registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
        }
    }
    

    2.4 加载Spring容器

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

    public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        /**
         * spring容器,相当于加载apllicationContext.xml
         * @return
         */
        @Override
        protected Class<?>[] getRootConfigClasses() {
            // 指定rootContext的配置类
            return new Class<?>[]{ApplicationConfig.class};
        }
    
        /**
         * servletContext,相当于加载springmvc.xml配置文件
         * @return
         */
        @Override
        protected Class<?>[] getServletConfigClasses() {
            // 指定servletContext的配置类
            return new Class<?>[]{WebConfig.class};
        }
    
        /**
         * url-mapping
         * @return
         */
        @Override
        protected String[] getServletMappings() {
            return new String[] {"/"};
        }
    }
    

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

    <web-app>
        <listener>
            <listner-class>
                org.springframework.web.context.ContextLoaderListener
            </listner-class>
        </listener>
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/application-context.xml</param-value>
        </context-param>
        <servlet>
            <servlet-name>springmvc</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>/WEB-INF/spring-mvc.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>springmvc</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>
    

    2.5 实现认证功能

    2.5.1 认证页面

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

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>用户登录</title>
    </head>
    <body>
        <form action="login" method="post">
            用户名:<input type="text" name="username"><br>
            密&nbsp;&nbsp;&nbsp;码:<input type="password" name="password"><br>
            <input type="submit" value="登录">
        </form>
    </body>
    </html>
    

    WebConfig中添加如下配置,将“/”直接定向到login.jsp页面:

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // 将"/"定向到"/login"
       registry.addViewController("/").setViewName("login");
    }
    

    2.5.2 启动项目

    pom配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <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">
    
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.pengjs.book.admin.security</groupId>
        <artifactId>security-springmvc</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>war</packaging>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>5.1.5.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
                <!-- 将来需要实用tomcat容器运行,所以scope为provided -->
                <scope>provided</scope>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.8</version>
            </dependency>
        </dependencies>
    
        <build>
            <finalName>security-springmvc</finalName>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <groupId>org.apache.tomcat.maven</groupId>
                        <artifactId>tomcat7-maven-plugin</artifactId>
                        <version>2.2</version>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>3.8.1</version>
                        <configuration>
                            <source>1.8</source>
                            <target>1.8</target>
                        </configuration>
                    </plugin>
    
                    <plugin>
                        <artifactId>maven-resources-plugin</artifactId>
                        <version>3.0.2</version>
                        <configuration>
                            <encoding>utf-8</encoding>
                            <useDefaultDelimiters>true</useDefaultDelimiters>
                            <resources>
                                <resource>
                                    <directory>src/main/resources</directory>
                                    <filtering>true</filtering>
                                    <includes>
                                        <include>**/*</include>
                                    </includes>
                                </resource>
                                <resource>
                                    <directory>src/main/java</directory>
                                    <includes>
                                        <include>**/*.xml</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </plugin>
                </plugins>
            </pluginManagement>
        </build>
    </project>
    

    配置maven启动:


    配置maven启动 用户登录

    2.5.3 认证接口

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

    1. 定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出异常:
    public interface AuthenticationService {
    
        /**
         * 用户认证,校验用户信息是否合法
         * @param authenticationRequest 用户认证请求,账号和密码
         * @return 认证成功后的用户信息
         */
        UserDto authentication(AuthenticationRequest authenticationRequest);
    }
    
    1. 认证请求结构:
    /**
     * 用户身份信息
     */
    @Data
    public class AuthenticationRequest {
    
        /**
         * 用户名
         */
        private String username;
    
        /**
         * 密码
         */
        private String password;
    
    }
    
    1. 认证成功后返回的用户详细信息,也就是当前登录用户的信息:
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class UserDto {
    
        public static final String SESSION_USER_KEY = "_user";
    
        private String id;
        private String username;
        private String password;
        private String fullname;
        private String mobile;
    
        /**
         * 用户权限
         */
        private Set<String> authorities;
    
    }
    
    1. 认证接口实现类AuthenticationServiceImpl
    @Service
    public class AuthenticationServiceImpl implements AuthenticationService {
    
        @Override
        public UserDto authentication(AuthenticationRequest authenticationRequest) {
            // 校验参数是否为空
            if (null == authenticationRequest || StringUtils.isEmpty(authenticationRequest.getUsername())
                    || StringUtils.isEmpty(authenticationRequest.getPassword())) {
                throw new RuntimeException("账号密码为空");
            }
            UserDto userDto = getUserDto(authenticationRequest.getUsername());
            // 判断用户是否为空
            if (null == userDto) {
                throw new RuntimeException("查询不到该用户");
            }
            // 校验密码
            if (!authenticationRequest.getPassword().equals(userDto.getPassword())) {
                throw new RuntimeException("账号或者密码错误");
            }
            // 认证通过
            return userDto;
        }
    
        /**
         * 模拟用户查询
         *
         * @param username 用户名
         * @return UserDto
         */
        private UserDto getUserDto(String username) {
            return userMap.get(username);
        }
    
        private Map<String, UserDto> userMap = new HashMap<>();
    
        {
            // 不同的用户,不同的权限
            Set<String> authorities1 = new HashSet<>();
            // p1权限标识符,和/r/r1对应
            authorities1.add("p1");
            // p2权限标识符,和/r/r2对应
            Set<String> authorities2 = new HashSet<>();
            authorities2.add("p2");
            userMap.put("zhangsan", new UserDto("1000", "zhangsan", "123", "张三", "133433", authorities1));
            userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "144553", authorities2));
        }
    
    }
    
    1. LoginController
    @RestController
    public class LoginController {
    
        @Autowired
        private AuthenticationService authenticationService;
    
        @PostMapping(value = "/login", produces = "text/plain;charset=utf-8")
        public String login(AuthenticationRequest authenticationRequest, HttpSession session) {
            UserDto userDto = authenticationService.authentication(authenticationRequest);
            // 存入session
            session.setAttribute(UserDto.SESSION_USER_KEY, userDto);
            return userDto.getFullname() + "登录成功";
        }
    }
    

    2.6 实现会话功能

    会话是指用户登录系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。
    认证的目的是对系统资源的保护,每次对资源的访问,系统必须得直到是谁在访问资源,才能对该请求进行合法拦截。因此,在认证成功后,一般会把认证后的用户信息放入Session中,在后续的请求中,系统能够从Session中获取到当前用户,用这样的方式来实现会话机制。

    1. 增加会话控制
      首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。
    public static final String SESSION_USER_KEY = "_user";
    
    1. 然后在LoginController中,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时session只为无效。
    /**
     * 退出登录
     * @param session
     * @return
     */
    @GetMapping(value = "logout", produces = {"text/plain;charset=utf-8"})
    public String logout(HttpSession session) {
        session.invalidate();
        return "退出成功";
    }
    
    1. LoginController中增加测试资源,如果已经登录过则会在session中获取“_user”对应的UserDto信息,否则为空。
    /**
     * 测试资源1
     * @param session
     * @return
     */
    @GetMapping(value = "/r/r1", produces = {"text/plain;charset=utf-8"})
    public String r1(HttpSession session) {
        String fullname = null;
        Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
        if (null != userObj) {
            fullname = ((UserDto) userObj).getFullname();
        } else {
            fullname = "匿名";
        }
        return fullname + " 访问资源1";
    }
    

    2.7 实现授权功能

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

    • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
    • 登录用户访问拦截:根据用户的权限决定能否访问某些资源。
    1. 增加权限数据
      为了实现这样的功能,需要在UserDto中增加权限属性,用于表示该登录用户所拥有的权限:
    增加权限数据
    1. AuthenticationServiceImpl中为模拟用户初始化权限,其中给了张三p1权限,给了李四p2权限。
    初始化用户权限
    1. 编写拦截器SimpleAuthenticationInterceptor
    @Component
    public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
    
        /**
         * 调用Controller之前拦截
         * 校验用户请求的URL是否在用户的权限范围内
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 取出用户身份信息
            Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
            if (null == object) {
                // 没有认证,提示登录
                writeContent(response, "请登录");
                return false;
            }
            UserDto userDto = (UserDto) object;
            // 根据请求的URL
            String requestURI = request.getRequestURI();
            if (userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")) {
                return true;
            }
            if (userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")) {
                return true;
            }
            writeContent(response, "没有权限,拒绝访问");
            return false;
        }
    
        /**
         * 响应信息给前端
         * @param response
         * @param msg
         */
        private void writeContent(HttpServletResponse response, String msg) throws IOException {
            response.setContentType("text/html;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(msg);
            writer.close();
        }
    }
    
    1. WebConfig中注册拦截器,是拦截器生效:
    注册拦截器

    3. 小结

    基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。而在正常项目中,我们往往会考虑使用第三方安全框架(如spring security,shiro等)来实现认证授权功能,因为这样能一定程度提高生产力,提高软件标准化程度,另外,这下框架的可扩展性考虑的非常全面。但是缺点也非常明显,这些同用户组件为了提高支持范围会增加很多可能不需要的功能,结构上也会比较抽象,如果不了解它,一旦出现问题,将很难定位。

    相关文章

      网友评论

          本文标题:基于Session的认证方式

          本文链接:https://www.haomeiwen.com/subject/dprxgktx.html