1. 认证流程
基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session中(当前会话)中,发给客户端对应的session_id存放到cookie中,这样用户请求时带上session_id就可以验证服务器端是否存在session数据,以完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
基于session的认证方式基于Session的认证机制由Servlet规范定制,Servlet容器已经实现,用户通过HttpSession的操作方式即可实现,如下是HttpSession相关的操作API:
HttpSession相关的操作API2. 创建工程
本案例工程使用maven进行构建,使用SpringMVC、Servlet3.0实现。
2.1 创建maven工程
创建maven工程security-springmvc,工程结构如下:
工程结构2.2 Spring容器配置
在config包下定义ApplicationConfig
,它对应web.xml中的ContextLoaderListener
/**
* 相当于apllicationContext.xml配置文件
*/
@Configuration
@ComponentScan(basePackages = "com.pengjs.book.admin.security",
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {
// 配置除了Controller的其他bean,如:数据库连接池、事务管理器、业务bean等
}
2.3 ServletContext配置
本案例采用Servlet3.0无web.xml方式,在config包下定义WebConfig
,它对应于DispatcherServlet
配置
/**
* 相当于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
,此类实现WebApplicationInitializer
接口,Spring容器启动时加载WebApplicationInitializer
接口的所有实现类。
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* spring容器,相当于加载apllicationContext.xml
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
// 指定rootContext的配置类
return new Class<?>[]{ApplicationConfig.class};
}
/**
* servletContext,相当于加载springmvc.xml配置文件
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
// 指定servletContext的配置类
return new Class<?>[]{WebConfig.class};
}
/**
* url-mapping
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
SpringApplicationInitializer
相当于web.xml,使用了Servlet3.0开发则不需要要再定义web.xml,ApplicationConfig
对应以下配置的application-context.xml,WebConfig
对应以下配置的spring-mvc.xml,web.xml的参考内容:
<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 实现认证功能
2.5.1 认证页面
在webapp/WEB-INF/view下定义login.jsp,本案例知识测试认证流程,没有添加css样式,页面实现可填入用户名、密码,触发登录提交表单信息至/login,内容如下:
<%@ 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>
密 码:<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 启动项目
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>
<!-- 将来需要实用tomcat容器运行,所以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启动:
配置maven启动 用户登录
2.5.3 认证接口
用户进入认证页面,熟肉账号和密码,点击登录,请求/login进行身份认证。
- 定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出异常:
public interface AuthenticationService {
/**
* 用户认证,校验用户信息是否合法
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功后的用户信息
*/
UserDto authentication(AuthenticationRequest authenticationRequest);
}
- 认证请求结构:
/**
* 用户身份信息
*/
@Data
public class AuthenticationRequest {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
- 认证成功后返回的用户详细信息,也就是当前登录用户的信息:
@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;
/**
* 用户权限
*/
private Set<String> authorities;
}
- 认证接口实现类
AuthenticationServiceImpl
:
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
// 校验参数是否为空
if (null == authenticationRequest || StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())) {
throw new RuntimeException("账号密码为空");
}
UserDto userDto = getUserDto(authenticationRequest.getUsername());
// 判断用户是否为空
if (null == userDto) {
throw new RuntimeException("查询不到该用户");
}
// 校验密码
if (!authenticationRequest.getPassword().equals(userDto.getPassword())) {
throw new RuntimeException("账号或者密码错误");
}
// 认证通过
return userDto;
}
/**
* 模拟用户查询
*
* @param username 用户名
* @return UserDto
*/
private UserDto getUserDto(String username) {
return userMap.get(username);
}
private Map<String, UserDto> userMap = new HashMap<>();
{
// 不同的用户,不同的权限
Set<String> authorities1 = new HashSet<>();
// p1权限标识符,和/r/r1对应
authorities1.add("p1");
// p2权限标识符,和/r/r2对应
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));
}
}
-
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 实现会话功能
会话是指用户登录系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。
认证的目的是对系统资源的保护,每次对资源的访问,系统必须得直到是谁在访问资源,才能对该请求进行合法拦截。因此,在认证成功后,一般会把认证后的用户信息放入Session中,在后续的请求中,系统能够从Session中获取到当前用户,用这样的方式来实现会话机制。
- 增加会话控制
首先在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。
public static final String SESSION_USER_KEY = "_user";
- 然后在
LoginController
中,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时session只为无效。
/**
* 退出登录
* @param session
* @return
*/
@GetMapping(value = "logout", produces = {"text/plain;charset=utf-8"})
public String logout(HttpSession session) {
session.invalidate();
return "退出成功";
}
-
LoginController
中增加测试资源,如果已经登录过则会在session中获取“_user”对应的UserDto信息,否则为空。
/**
* 测试资源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 实现授权功能
现在我们已经完成了用户身份凭证及登录的状态保持,并且也知道了如何获取当前登录用户(从session中获取)的信息,接下来,用具访问系统需要经过授权,即需要完成以下功能:
- 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
- 登录用户访问拦截:根据用户的权限决定能否访问某些资源。
- 增加权限数据
为了实现这样的功能,需要在UserDto中增加权限属性,用于表示该登录用户所拥有的权限:
- 在
AuthenticationServiceImpl
中为模拟用户初始化权限,其中给了张三p1权限,给了李四p2权限。
- 编写拦截器
SimpleAuthenticationInterceptor
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
/**
* 调用Controller之前拦截
* 校验用户请求的URL是否在用户的权限范围内
* @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) {
// 没有认证,提示登录
writeContent(response, "请登录");
return false;
}
UserDto userDto = (UserDto) object;
// 根据请求的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, "没有权限,拒绝访问");
return false;
}
/**
* 响应信息给前端
* @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();
}
}
-
WebConfig
中注册拦截器,是拦截器生效:
3. 小结
基于Session的认证方式是一种常见的认证方式,至今还有非常多的系统在使用。而在正常项目中,我们往往会考虑使用第三方安全框架(如spring security,shiro等)来实现认证授权功能,因为这样能一定程度提高生产力,提高软件标准化程度,另外,这下框架的可扩展性考虑的非常全面。但是缺点也非常明显,这些同用户组件为了提高支持范围会增加很多可能不需要的功能,结构上也会比较抽象,如果不了解它,一旦出现问题,将很难定位。
网友评论