zoukankan      html  css  js  c++  java
  • 基于session的传统认证授权详解

    基于session的传统认证授权详解


    概念部分

    1.认证

    用户认证是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。

    2.会话

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

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

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

    3.授权

    拿微信举例,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友 圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

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

    项目代码部分

    创建工程

    以idea为例file->new project->maven->next->填写groupid和artifactid->项目名和项目位置->finish

    导入依赖

    设置maven(省略)

    pom.xml

    <?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.chen.security</groupId>
        <artifactId>security</artifactId>
        <version>1.0-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.0.1</version>
                <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.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                            <source>1.8</source>
                            <target>1.8</target>
                        </configuration>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.tomcat.maven</groupId>
                        <artifactId>tomcat7-maven-plugin</artifactId>
                        <version>2.2</version>
                    </plugin>
                    <plugin>
                        <artifactId>maven-resources-plugin</artifactId>
                        <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>
    

    目录结构

    Spring 容器配置

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

    @Configuration
    @ComponentScan(basePackages = "com.chen.security",
            excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
    public class ApplicationConfig {
    }
    

    servletContext配置

    @Configuration
    @EnableWebMvc
    @ComponentScan(basePackages = "com.chen.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/views/");
    viewResolver.setSuffix(".jsp");
    return viewResolver;
    }
    }
    
    

    加载 Spring容器

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

    public class SpringApplicationInitializer extends
    AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { ApplicationConfig.class };//指定rootContext的配置类
    }
    @Override
    protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class }; //指定servletContext的配置类
    }
    @Override
    protected String[] getServletMappings() {
    return new String [] {"/"};
    }
    }
    
    

    实现认证功能

    认证页面

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

    <%@ page contentType="text/html;charset=UTF‐8" pageEncoding="utf‐8" %>
    <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) {
    registry.addViewController("/").setViewName("login");
    }
    

    配置如下运行

    运行访问

    定义认证接口以及相关实体类(文件具体位置见目录结构)

    AuthenticationService.java

    /**
    * 认证服务
    */
    public interface AuthenticationService {
    /**
    * 用户认证
    * @param authenticationRequest 用户认证请求
    * @return 认证成功的用户信息
    */
    UserDto authentication(AuthenticationRequest authenticationRequest);
    }
    
    

    AuthenticationRequest.java

    @Data
    public class AuthenticationRequest {
    /**
    * 用户名
    */
    private String username;
    /**
    * 密码
    */
    private String password;
    }
    
    

    UserDto.java

    /**
    * 当前登录用户信息
    */
    @Data
    @AllArgsConstructor
    public class UserDto {
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    }
    
    

    实现这个接口

    AuthenticationServiceimpl.java

    @Service
    public class AuthenticationServiceImpl implements AuthenticationService{
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
    if(authenticationRequest == null
    || StringUtils.isEmpty(authenticationRequest.getUsername())
    || StringUtils.isEmpty(authenticationRequest.getPassword())){
    throw new RuntimeException("账号或密码为空");
    }
    UserDto userDto = getUserDto(authenticationRequest.getUsername());
    if(userDto == null){
    throw new RuntimeException("查询不到该用户");
    }
    if(!authenticationRequest.getPassword().equals(userDto.getPassword())){
    throw new RuntimeException("账号或密码错误");
    }
    return userDto;
    }
    //模拟用户查询
    public UserDto getUserDto(String username){
    return userMap.get(username);
    }
    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
    userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
    userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
    }
    }
    
    

    LoginController.java

    @RestController
    public class LoginController {
    @Autowired
    private AuthenticationService authenticationService;
    /**
    * 用户登录
    * @param authenticationRequest 登录请求
    * @return
    */
    @PostMapping(value = "/login",produces = {"text/plain;charset=UTF‐8"})
    public String login(AuthenticationRequest authenticationRequest){
    UserDetails userDetails = authenticationService.authentication(authenticationRequest);
    return userDetails.getFullname() + " 登录成功";
    }
    }
    

    测试

    登录设定的账号密码登录可以成功,否则登陆失败

    实现会话功能

    在UserDto.java中新增一个常量作为Session的key

    public static final String SESSION_USER_KEY = "_user";
    

    修改LoginController

    /**
    * 用户登录
    * @param authenticationRequest 登录请求
    * @param session http会话
    * @return
    */
    @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.getUsername() + "登录成功";
    }
    @GetMapping(value = "logout",produces = "text/plain;charset=utf‐8")
    public String logout(HttpSession session){
    session.invalidate();
    return "退出成功";
    }
    
    

    增加测试资源LoginController

    /**
    * 测试资源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(userObj != null){
    fullname = ((UserDto)userObj).getFullname();
    }else{
    fullname = "匿名";
    }
    return fullname + " 访问资源1";
    }
    
    测试

    未登录情况下访问/r/r1显示 匿名访问资源1

    登录情况下访问则显示 登录者的全名 访问资源1

    实现授权功能

    修改UserDto如下

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

    并在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限

    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
    Set<String> authorities1 = new HashSet<>();
    authorities1.add("p1");
    Set<String> authorities2 = new HashSet<>();
    authorities2.add("p2");
    userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张
    三","133443",authorities1));
    userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }
    private UserDetails getUserDetails(String username) {
    return userDetailsMap.get(username);
    }
    
    

    我们想实现针对不同的用户能访问不同的资源,前提是得有多个资源,因此在LoginController中增加测试资源2

    /**
    * 测试资源2
    * @param session
    * @return
    */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF‐8"})
    public String r2(HttpSession session){
    String fullname = null;
    Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
    if(userObj != null){
    fullname = ((UserDto)userObj).getFullname();
    }else{
    fullname = "匿名";
    }
    return fullname + " 访问资源2";
    }
    
    

    增加拦截器

    在interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截: 1、校验用户是否登录 2、校验用户是否拥有操作权限

    @Component
    public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //在这个方法中校验用户请求的url是否在用户的权限范围内
            //取出用户身份信息
            Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
            if(object==null){
                //没有认证提示登录
                writeContent(response,"请登录");
            }
    
            UserDto userDto=(UserDto) object;
            //获取用户权限
            //获取请求的uri
            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;
        }
        //响应信息给客户端
        private void writeContent(HttpServletResponse response, String msg) throws IOException {
            response.setContentType("text/html;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.println(msg);
            writer.close();
        }
    }
    

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

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
    }
    
    

    测试

    未登录情况下,/r/r1与/r/r2均提示 “请先登录”。 张三登录情况下,由于张三有p1权限,因此可以访问/r/r1,张三没有p2权限,访问/r/r2时提示 “权限不足 “。 李四登录情况下,由于李四有p2权限,因此可以访问/r/r2,李四没有p1权限,访问/r/r1时提示 “权限不足 “。 测试结果全部符合预期结果。

  • 相关阅读:
    Java 线程间通信 —— 等待 / 通知机制
    Java 线程基础
    Java 内存模型
    Java 并发机制底层实现 —— volatile 原理、synchronize 锁优化机制、原子操作
    优秀程序员的博客有哪些?
    程序员五一被拉去相亲,结果彻底搞懂了HTTP常用状态码
    【Redis破障之路】三:Redis单线程架构
    【Redis破障之路】一:强大的Redis
    MySQL提升笔记(4)InnoDB存储结构
    MySQL提升笔记(3)日志文件详解
  • 原文地址:https://www.cnblogs.com/chenguosong/p/13633246.html
Copyright © 2011-2022 走看看