1. 程式人生 > 實用技巧 >基於session的傳統認證授權詳解

基於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.itheima.security</groupId>
    <artifactId>security-mvc</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.itheima.security.springmvc",
        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){
北京市昌平區建材城西路金燕龍辦公樓一層 電話:400-618-9090
(2)增加測試資源
修改LoginController,增加測試資源1,它從當前會話session中獲取當前登入使用者,並返回提示資訊給前臺。
(3)測試
未登入情況下直接訪問測試資源/r/r1:
成功登入的情況下訪問測試資源/r/r1:
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時提示 “許可權不足 “。 測試結果全部符合預期結果。