基于Session的認(rèn)證方式

1. 認(rèn)證流程

基于Session認(rèn)證方式的流程是,用戶認(rèn)證成功后,在服務(wù)端生成用戶相關(guān)的數(shù)據(jù)保存在session中(當(dāng)前會(huì)話)中,發(fā)給客戶端對(duì)應(yīng)的session_id存放到cookie中,這樣用戶請(qǐng)求時(shí)帶上session_id就可以驗(yàn)證服務(wù)器端是否存在session數(shù)據(jù),以完成用戶的合法校驗(yàn),當(dāng)用戶退出系統(tǒng)或session過期銷毀時(shí),客戶端的session_id也就無效了。

基于session的認(rèn)證方式

基于Session的認(rèn)證機(jī)制由Servlet規(guī)范定制,Servlet容器已經(jīng)實(shí)現(xiàn),用戶通過HttpSession的操作方式即可實(shí)現(xiàn),如下是HttpSession相關(guān)的操作API:

HttpSession相關(guān)的操作API

2. 創(chuàng)建工程

本案例工程使用maven進(jìn)行構(gòu)建,使用SpringMVC、Servlet3.0實(shí)現(xiàn)。

2.1 創(chuàng)建maven工程

創(chuàng)建maven工程security-springmvc,工程結(jié)構(gòu)如下:

工程結(jié)構(gòu)

2.2 Spring容器配置

在config包下定義ApplicationConfig,它對(duì)應(yīng)web.xml中的ContextLoaderListener

/**
 * 相當(dāng)于apllicationContext.xml配置文件
 */
@Configuration
@ComponentScan(basePackages = "com.pengjs.book.admin.security",
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {
    // 配置除了Controller的其他bean,如:數(shù)據(jù)庫(kù)連接池、事務(wù)管理器、業(yè)務(wù)bean等

}

2.3 ServletContext配置

本案例采用Servlet3.0無web.xml方式,在config包下定義WebConfig,它對(duì)應(yīng)于DispatcherServlet配置

/**
 * 相當(dāng)于springmvc.xml配置文件
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.pengjs.book.admin.security",
        includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

    /**
     * 配置視圖解析器
     * @return
     */
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 將"/"定向到"/login"
        registry.addViewController("/").setViewName("login");
    }

    /**
     * 添加攔截器,讓攔截器生效
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // login接口不攔截,只攔截:/r/**
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
    }
}

2.4 加載Spring容器

在init包下定義Spring容器初始化類SpringApplicationInitializer,此類實(shí)現(xiàn)WebApplicationInitializer接口,Spring容器啟動(dòng)時(shí)加載WebApplicationInitializer接口的所有實(shí)現(xiàn)類。

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * spring容器,相當(dāng)于加載apllicationContext.xml
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 指定rootContext的配置類
        return new Class<?>[]{ApplicationConfig.class};
    }

    /**
     * servletContext,相當(dāng)于加載springmvc.xml配置文件
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        // 指定servletContext的配置類
        return new Class<?>[]{WebConfig.class};
    }

    /**
     * url-mapping
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }
}

SpringApplicationInitializer相當(dāng)于web.xml,使用了Servlet3.0開發(fā)則不需要要再定義web.xml,ApplicationConfig對(duì)應(yīng)以下配置的application-context.xml,WebConfig對(duì)應(yīng)以下配置的spring-mvc.xml,web.xml的參考內(nèi)容:

<web-app>
    <listener>
        <listner-class>
            org.springframework.web.context.ContextLoaderListener
        </listner-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/application-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

2.5 實(shí)現(xiàn)認(rèn)證功能

2.5.1 認(rèn)證頁面

在webapp/WEB-INF/view下定義login.jsp,本案例知識(shí)測(cè)試認(rèn)證流程,沒有添加css樣式,頁面實(shí)現(xiàn)可填入用戶名、密碼,觸發(fā)登錄提交表單信息至/login,內(nèi)容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<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) {
   // 將"/"定向到"/login"
   registry.addViewController("/").setViewName("login");
}

2.5.2 啟動(dòng)項(xiàng)目

pom配置:

<?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.pengjs.book.admin.security</groupId>
    <artifactId>security-springmvc</artifactId>
    <version>0.0.1-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.1.0</version>
            <!-- 將來需要實(shí)用tomcat容器運(yùn)行,所以scope為provided -->
            <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.tomcat.maven</groupId>
                    <artifactId>tomcat7-maven-plugin</artifactId>
                    <version>2.2</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>

                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                    <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>

配置maven啟動(dòng):


配置maven啟動(dòng)
用戶登錄

2.5.3 認(rèn)證接口

用戶進(jìn)入認(rèn)證頁面,熟肉賬號(hào)和密碼,點(diǎn)擊登錄,請(qǐng)求/login進(jìn)行身份認(rèn)證。

  1. 定義認(rèn)證接口,此接口用于對(duì)傳來的用戶名、密碼校驗(yàn),若成功則返回該用戶的詳細(xì)信息,否則拋出異常:
public interface AuthenticationService {

    /**
     * 用戶認(rèn)證,校驗(yàn)用戶信息是否合法
     * @param authenticationRequest 用戶認(rèn)證請(qǐng)求,賬號(hào)和密碼
     * @return 認(rèn)證成功后的用戶信息
     */
    UserDto authentication(AuthenticationRequest authenticationRequest);
}
  1. 認(rèn)證請(qǐng)求結(jié)構(gòu):
/**
 * 用戶身份信息
 */
@Data
public class AuthenticationRequest {

    /**
     * 用戶名
     */
    private String username;

    /**
     * 密碼
     */
    private String password;

}
  1. 認(rèn)證成功后返回的用戶詳細(xì)信息,也就是當(dāng)前登錄用戶的信息:
@Data
@AllArgsConstructor
@NoArgsConstructor
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;

    /**
     * 用戶權(quán)限
     */
    private Set<String> authorities;

}
  1. 認(rèn)證接口實(shí)現(xiàn)類AuthenticationServiceImpl
@Service
public class AuthenticationServiceImpl implements AuthenticationService {

    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        // 校驗(yàn)參數(shù)是否為空
        if (null == authenticationRequest || StringUtils.isEmpty(authenticationRequest.getUsername())
                || StringUtils.isEmpty(authenticationRequest.getPassword())) {
            throw new RuntimeException("賬號(hào)密碼為空");
        }
        UserDto userDto = getUserDto(authenticationRequest.getUsername());
        // 判斷用戶是否為空
        if (null == userDto) {
            throw new RuntimeException("查詢不到該用戶");
        }
        // 校驗(yàn)密碼
        if (!authenticationRequest.getPassword().equals(userDto.getPassword())) {
            throw new RuntimeException("賬號(hào)或者密碼錯(cuò)誤");
        }
        // 認(rèn)證通過
        return userDto;
    }

    /**
     * 模擬用戶查詢
     *
     * @param username 用戶名
     * @return UserDto
     */
    private UserDto getUserDto(String username) {
        return userMap.get(username);
    }

    private Map<String, UserDto> userMap = new HashMap<>();

    {
        // 不同的用戶,不同的權(quán)限
        Set<String> authorities1 = new HashSet<>();
        // p1權(quán)限標(biāo)識(shí)符,和/r/r1對(duì)應(yīng)
        authorities1.add("p1");
        // p2權(quán)限標(biāo)識(shí)符,和/r/r2對(duì)應(yīng)
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");
        userMap.put("zhangsan", new UserDto("1000", "zhangsan", "123", "張三", "133433", authorities1));
        userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "144553", authorities2));
    }

}
  1. LoginController
@RestController
public class LoginController {

    @Autowired
    private AuthenticationService authenticationService;

    @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.getFullname() + "登錄成功";
    }
}

2.6 實(shí)現(xiàn)會(huì)話功能

會(huì)話是指用戶登錄系統(tǒng)后,系統(tǒng)會(huì)記住該用戶的登錄狀態(tài),他可以在系統(tǒng)連續(xù)操作直到退出系統(tǒng)的過程。
認(rèn)證的目的是對(duì)系統(tǒng)資源的保護(hù),每次對(duì)資源的訪問,系統(tǒng)必須得直到是誰在訪問資源,才能對(duì)該請(qǐng)求進(jìn)行合法攔截。因此,在認(rèn)證成功后,一般會(huì)把認(rèn)證后的用戶信息放入Session中,在后續(xù)的請(qǐng)求中,系統(tǒng)能夠從Session中獲取到當(dāng)前用戶,用這樣的方式來實(shí)現(xiàn)會(huì)話機(jī)制。

  1. 增加會(huì)話控制
    首先在UserDto中定義一個(gè)SESSION_USER_KEY,作為Session中存放登錄用戶信息的key。
public static final String SESSION_USER_KEY = "_user";
  1. 然后在LoginController中,認(rèn)證成功后,將用戶信息放入當(dāng)前會(huì)話。并增加用戶登出方法,登出時(shí)session只為無效。
/**
 * 退出登錄
 * @param session
 * @return
 */
@GetMapping(value = "logout", produces = {"text/plain;charset=utf-8"})
public String logout(HttpSession session) {
    session.invalidate();
    return "退出成功";
}
  1. LoginController中增加測(cè)試資源,如果已經(jīng)登錄過則會(huì)在session中獲取“_user”對(duì)應(yīng)的UserDto信息,否則為空。
/**
 * 測(cè)試資源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 (null != userObj) {
        fullname = ((UserDto) userObj).getFullname();
    } else {
        fullname = "匿名";
    }
    return fullname + " 訪問資源1";
}

2.7 實(shí)現(xiàn)授權(quán)功能

現(xiàn)在我們已經(jīng)完成了用戶身份憑證及登錄的狀態(tài)保持,并且也知道了如何獲取當(dāng)前登錄用戶(從session中獲取)的信息,接下來,用具訪問系統(tǒng)需要經(jīng)過授權(quán),即需要完成以下功能:

  • 匿名用戶(未登錄用戶)訪問攔截:禁止匿名用戶訪問某些資源。
  • 登錄用戶訪問攔截:根據(jù)用戶的權(quán)限決定能否訪問某些資源。
  1. 增加權(quán)限數(shù)據(jù)
    為了實(shí)現(xiàn)這樣的功能,需要在UserDto中增加權(quán)限屬性,用于表示該登錄用戶所擁有的權(quán)限:
增加權(quán)限數(shù)據(jù)
  1. AuthenticationServiceImpl中為模擬用戶初始化權(quán)限,其中給了張三p1權(quán)限,給了李四p2權(quán)限。
初始化用戶權(quán)限
  1. 編寫攔截器SimpleAuthenticationInterceptor
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {

    /**
     * 調(diào)用Controller之前攔截
     * 校驗(yàn)用戶請(qǐng)求的URL是否在用戶的權(quán)限范圍內(nèi)
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 取出用戶身份信息
        Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if (null == object) {
            // 沒有認(rèn)證,提示登錄
            writeContent(response, "請(qǐng)登錄");
            return false;
        }
        UserDto userDto = (UserDto) object;
        // 根據(jù)請(qǐng)求的URL
        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, "沒有權(quán)限,拒絕訪問");
        return false;
    }

    /**
     * 響應(yīng)信息給前端
     * @param response
     * @param msg
     */
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(msg);
        writer.close();
    }
}
  1. WebConfig中注冊(cè)攔截器,是攔截器生效:
注冊(cè)攔截器

3. 小結(jié)

基于Session的認(rèn)證方式是一種常見的認(rèn)證方式,至今還有非常多的系統(tǒng)在使用。而在正常項(xiàng)目中,我們往往會(huì)考慮使用第三方安全框架(如spring security,shiro等)來實(shí)現(xiàn)認(rèn)證授權(quán)功能,因?yàn)檫@樣能一定程度提高生產(chǎn)力,提高軟件標(biāo)準(zhǔn)化程度,另外,這下框架的可擴(kuò)展性考慮的非常全面。但是缺點(diǎn)也非常明顯,這些同用戶組件為了提高支持范圍會(huì)增加很多可能不需要的功能,結(jié)構(gòu)上也會(huì)比較抽象,如果不了解它,一旦出現(xiàn)問題,將很難定位。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容