基于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.chen.security</groupId>
    <artifactId>security</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.chen.security",
        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){
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时提示 “权限不足 “。 测试结果全部符合预期结果。

posted @ 2020-09-08 16:18  陈国松  阅读(742)  评论(1编辑  收藏  举报