【認證與受權】二、基於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的學習。加油。

(完)

相關文章
相關標籤/搜索