【認證與授權】2、基於session的認證方式


這一篇將通過一個簡單的web項目實現基於Session的認證授權方式,也是以往傳統項目的做法。
先來復習一下流程

用戶認證通過以后,在服務端生成用戶相關的數據保存在當前會話(Session)中,發給客戶端的數據將通過session_id 存放在cookie中。在后續的請求操作中,客戶端將帶上session_id,服務端就可以驗證是否存在了,並可拿到其中的數據校驗其合法性。當用戶退出系統或session_id到期時,服務端則會銷毀session_id。具體可查看上篇的基本概念了解。

1. 創建工程

本案例為了方便,直接使用springboot快速創建一個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 實現認證功能

實現認證功能,我們一般需要這樣幾個資源

  • 認證的入口(認證頁面)
  • 認證的憑證(用戶的憑證信息)
  • 認證邏輯(如何才算認證成功)

認證頁面
也就是我們常說的登錄頁

<!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>

頁面控制器
現在有了認證頁面,那我如果才可以進入到認證頁面呢,同時我點擊登陸后,下一步該做什么呢?

@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
}

同時我們還需要一個狀態用戶信息的對象User

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
}

有了用戶了,有了入口了,接下來就是對這些數據的處理,看是否如何認證條件了

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

這里我們模擬了兩個用戶,一個是正常使用的賬號,還有個賬號因為某些特殊的原因被封禁了,我們一起來測試一下。

啟動項目在客戶端輸入localhost:8080 會直接跳轉到認證頁面

login1.png

我們分別嘗試不同的賬戶密碼登錄看具體顯示什么信息。

1、數據的密碼不正確

error1.png

2、賬戶被禁用

error2.png

3、數據正確的用戶名和密碼

success1.png

此時我們的測試均已符合預期,能夠將正確的信息反饋給用戶。這也是最基礎的認證功能,用戶能夠通過系統的認證,說明他是該系統的合法用戶,但是用戶在后續的訪問過程中,我們需要知道到底是哪個用戶在操作呢,這時我們就需要引入到會話的功能呢。

1.3 實現會話功能

會話是指一個終端用戶與交互系統進行通訊的過程,比如從輸入賬戶密碼進入操作系統到退出操作系統就是一個會話過程。
1、增加會話的控制

關於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() + "你好!";
}

2、測試會話的效果

既然說用戶認證后,我們將用戶的信息保存在了服務端中,那我們就測試一下通過會話,服務端是否知道后續的操作是哪個用戶呢?我們添加一個獲取用戶信息的接口 /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 實現授權功能

既然我們知道了是誰在訪問用戶,接下來我們將對用戶訪問的資源進行控制。

  • 匿名用戶針對部分接口不可訪問,提示其認證后再訪問
  • 根據用戶擁有的權限對資源進行操作(資源查詢/資源更新)

1、實現匿名用戶不可訪問。

前面我們已經可以通過/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 "這是非匿名用戶訪問的資源";
}

這里我們就不盡興測試頁面的展示了。

2、根據用戶擁有的權限對資源進行操作(資源查詢/資源更新)

根據匿名用戶處理的方式,我們此時也可設置攔截器,對接口的權限和用戶的權限進行對比,通過后放行,不通過則提示。此時我們需要配置這樣幾個地方

  • 用戶所具有的權限
  • 一個權限對比的攔截器
  • 一個資源接口

改造用戶信息,使其具有相應的權限

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/**");
    }
}

此時我們就可以使用不同的用戶進行認證后訪問不同的資源來進行測試了。

2、總結

當然,這僅僅是最簡單的實踐,特別是權限處理這一塊,很多都是采取硬編碼的方式處理,旨在梳理流程相關信息。而在正式的生產環境中,我們將會采取更安全更靈活更容易擴展的方式處理,同時也會使用非常實用的安全框架進行企業級認證授權的處理,例如spring securityshiro等安全框架,在接下來的篇幅中,我們將進入到sping security的學習。加油。

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM