【认证与受权】二、基于session的认证方式

这一篇将经过一个简单的web项目实现基于Session的认证受权方式,也是以往传统项目的作法。
先来复习一下流程html

用户认证经过之后,在服务端生成用户相关的数据保存在当前会话(Session)中,发给客户端的数据将经过session_id 存放在cookie中。在后续的请求操做中,客户端将带上session_id,服务端就能够验证是否存在了,并可拿到其中的数据校验其合法性。当用户退出系统或session_id到期时,服务端则会销毁session_id。具体可查看上篇的基本概念了解。java

1. 建立工程

本案例为了方便,直接使用springboot快速建立一个web工程web

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">
   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>simple-mvc</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
</project>

1.2 实现认证功能

实现认证功能,咱们通常须要这样几个资源spring

  • 认证的入口(认证页面)
  • 认证的凭证(用户的凭证信息)
  • 认证逻辑(如何才算认证成功)

认证页面
也就是咱们常说的登陆页数据库

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登陆"/></div>
</form>
</body>
</html>

页面控制器
如今有了认证页面,那我若是才能够进入到认证页面呢,同时我点击登录后,下一步该作什么呢?apache

@Controller
public class LoginController {
  	// 认证逻辑处理
    @Autowired
    private AuthenticationService authenticationService;
  
		// 根路径直接跳转至认证页面
    @RequestMapping("/")
    public String loginUrl() {
        return "/login";
    }

		// 认证请求
    @RequestMapping("/login")
    @ResponseBody
    public String login(HttpServletRequest request) {
   AuthenticationRequest authenticationRequest = new AuthenticationRequest(request);
        User user = authenticationService.authentication(authenticationRequest);
        return user.getUsername() + "你好!";
    }
}

经过客户端传递来的参数进行处理安全

public class AuthenticationRequest {
    private String username;
    private String password;

    public AuthenticationRequest(HttpServletRequest request){
        username = request.getParameter("username");
        password = request.getParameter("password");
    }
    // 省略 setter getter
}

同时咱们还须要一个状态用户信息的对象Userspringboot

public class User {
    private Integer userId;
    private String username;
    private String password;
    private boolean enable;

    public User(Integer userId, String username, String password, boolean enable) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.enable = enable;
    }
		// 省略 setter getter
}

有了用户了,有了入口了,接下来就是对这些数据的处理,看是否如何认证条件了cookie

@Service
public class AuthenticationService{
		// 模拟数据库中保存的两个用户
    private static final Map<String, User> userMap = new HashMap<String, User>() {{
        put("admin", new User(1, "admin", "admin", true));
        put("spring", new User(2, "spring", "spring", false));
    }};

    private User loginByUserName(String userName) {
        return userMap.get(userName);
    }

    @Override
    public User authentication(AuthenticationRequest authenticationRequest) {
        if (authenticationRequest == null
                || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())) {
            throw new RuntimeException("帐号或密码为空");
        }
        User user = loginByUserName(authenticationRequest.getUsername());
        if (user == null) {
            throw new RuntimeException("用户不存在");
        }
        if(!authenticationRequest.getPassword().equals(user.getPassword())){
            throw new RuntimeException("密码错误");
        }
        if (!user.isEnable()){
            throw new RuntimeException("该帐户已被禁用");
        }
        return user;
    }
}

这里咱们模拟了两个用户,一个是正常使用的帐号,还有个帐号由于某些特殊的缘由被封禁了,咱们一块儿来测试一下。session

启动项目在客户端输入localhost:8080 会直接跳转到认证页面

login1.png

咱们分别尝试不一样的帐户密码登陆看具体显示什么信息。

一、数据的密码不正确

error1.png

二、帐户被禁用

error2.png

三、数据正确的用户名和密码

success1.png

此时咱们的测试均已符合预期,可以将正确的信息反馈给用户。这也是最基础的认证功能,用户可以经过系统的认证,说明他是该系统的合法用户,可是用户在后续的访问过程当中,咱们须要知道究竟是哪一个用户在操做呢,这时咱们就须要引入到会话的功能呢。

1.3 实现会话功能

会话是指一个终端用户与交互系统进行通信的过程,好比从输入帐户密码进入操做系统到退出操做系统就是一个会话过程。
一、增长会话的控制

关于session的操做,可参考HttpServletRqeust的相关API

前面引言中咱们提到了session_id的概念,与客户端的交互。
定义一个常量做为存放用户信息的key,同时在登陆成功后保存用户信息

privata finl static String USER_SESSION_KEY = "user_session_key";
@RequestMapping("/login")
@ResponseBody
public String login(HttpServletRequest request) {
	AuthenticationRequest authenticationRequest = new AuthenticationRequest(request);
	User user = authenticationService.authentication(authenticationRequest);
	request.getSession().setAttribute(USER_SESSION_KEY,user);
	return user.getUsername() + "你好!";
}

二、测试会话的效果

既然说用户认证后,咱们将用户的信息保存在了服务端中,那咱们就测试一下经过会话,服务端是否知道后续的操做是哪一个用户呢?咱们添加一个获取用户信息的接口 /getUser,看是否能后查询到当前登陆的用户信息

@ResponseBody
@RequestMapping("/getUser")
public String getUser(HttpServletRequest request){
  Object object = request.getSession().getAttribute("user_");
  if (object != null){
    User user = (User) object;
    return "当前访问用户为:" + user.getUsername();
  }
  return "匿名用户访问";
}

咱们经过客户端传递的信息,在服务端查询是否有用户信息,若是没有则是匿名用户的访问,若是有则返回该用户信息。

首先在不登陆下直接访问localhost:8080/getUser 返回匿名用户访问

登录后再访问返回当前访问用户为:admin

此时咱们已经能够看到当认证经过后,后续的访问服务端经过会话机制将知道当前访问的用户是说,这将便于咱们进一步处理对用户和资源的控制。

1.4 实现受权功能

既然咱们知道了是谁在访问用户,接下来咱们将对用户访问的资源进行控制。

  • 匿名用户针对部分接口不可访问,提示其认证后再访问
  • 根据用户拥有的权限对资源进行操做(资源查询/资源更新)

一、实现匿名用户不可访问。

前面咱们已经能够经过/getUser的接口示例中知道是不是匿名用户,那接下来咱们就对匿名用户进行拦截后跳转到认证页面。

public class NoAuthenticationInterceptor extends HandlerInterceptorAdapter {
    private final static String USER_SESSION_KEY = "user_session_key";
    // 前置拦截,在接口访问前处理
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object attribute = request.getSession().getAttribute(USER_SESSION_KEY);
        if (attribute == null){
            // 匿名访问 跳转到根路径下的login.html
            response.sendRedirect("/");
            return false;
        }
        return true;
    }
}

而后再将自定义的匿名用户拦截器,放入到web容器中使其生效

@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
    // 添加自定义拦截器,保护路径/protect 下的全部接口资源
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new 	NoAuthenticationInterceptor()).addPathPatterns("/protect/**");
    }
}

咱们保护/protect 下的全部接口资源,当匿名用户访问上述接口时,都将被系统跳转到认证页面进行认证后才能够访问。

@ResponseBody
@RequestMapping("/protect/getResource")
public String protectResource(HttpServletRequest request){
  return "这是非匿名用户访问的资源";
}

这里咱们就不尽兴测试页面的展现了。

二、根据用户拥有的权限对资源进行操做(资源查询/资源更新)

根据匿名用户处理的方式,咱们此时也可设置拦截器,对接口的权限和用户的权限进行对比,经过后放行,不经过则提示。此时咱们须要配置这样几个地方

  • 用户所具备的权限
  • 一个权限对比的拦截器
  • 一个资源接口

改造用户信息,使其具备相应的权限

public class User {
    private Integer userId;
    private String username;
    private String password;
    private boolean enable;
    // 授予权限
    private Set<String> authorities;

    public User(Integer userId, String username, String password, boolean enable,Set<String> authorities) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.enable = enable;
        this.authorities = authorities;
    }
}

从新设置用户

private static final Map<String, User> userMap = new HashMap<String, User>() {{
  Set<String> all =new HashSet<>();
  all.add("read");
  all.add("update");
  Set<String> read = new HashSet<>();
  read.add("read");

  put("admin", new User(1, "admin", "admin", true,all));
  put("spring", new User(2, "spring", "spring", false,read));
}};

咱们将admin用户设置最高权限,具备readupdate操做,spring用户只具备read权限

权限拦截器

public class AuthenticationInterceptor extends HandlerInterceptorAdapter {
    private final static String USER_SESSION_KEY = "user_session_key";
    // 前置拦截,在接口访问前处理
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object attribute = request.getSession().getAttribute(USER_SESSION_KEY);
        if (attribute == null) {
            writeContent(response,"匿名用户不可访问");
            return false;
        } else {
            User user = ((User) attribute);
            String requestURI = request.getRequestURI();
            if (user.getAuthorities().contains("read") && requestURI.contains("read")) {
                return true;
            }
            if (user.getAuthorities().contains("update") && requestURI.contains("update")) {
                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();
    }
}

在分别设置两个操做资源的接口

@ResponseBody
@RequestMapping("/protect/update")
public String protectUpdate(HttpServletRequest request){
  return "您正在更新资源信息";
}

@ResponseBody
@RequestMapping("/protect/read")
public String protectRead(HttpServletRequest request){
  return "您正在获取资源信息";
}

启用自定义拦截器

@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
    // 添加自定义拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new NoAuthenticationInterceptor()).addPathPatterns("/protect/**");
        registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/protect/**");
    }
}

此时咱们就可使用不一样的用户进行认证后访问不一样的资源来进行测试了。

二、总结

固然,这仅仅是最简单的实践,特别是权限处理这一块,不少都是采起硬编码的方式处理,旨在梳理流程相关信息。而在正式的生产环境中,咱们将会采起更安全更灵活更容易扩展的方式处理,同时也会使用很是实用的安全框架进行企业级认证受权的处理,例如spring securityshiro等安全框架,在接下来的篇幅中,咱们将进入到sping security的学习。加油。

(完)