本文正在参与「金石计划 . 瓜分6万现金大奖」

Spring Security系列文章

  • 认证与授权之Cookie、Session、Token、JWT
  • 依据Session的认证与授权实践

依据Session的认证办法

依据 session 的认证办法如下图:

基于Session的认证与授权实践

依据 Session 的认证机制由 Servlet 规范定制,Servlet 容器已完结,用户通过 HttpSession 的操作办法即可完结,如下是 HttpSession 相关的操作API。

基于Session的认证与授权实践

创立工程

本项目运用 maven 建立,运用 SpringMVC、Servlet3.0 完结。

创立maven工程

1、导入依赖

<?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>
    <artifactId>spring-security-study</artifactId>
    <groupId>com.msdn.security</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>springmvc-session</artifactId>
  <packaging>war</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.3.23</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.20</version>
    </dependency>
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>5.8.5</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Servlet Context装备

本事例采用 Servlet3.0 无 web.xml 办法,在 config 包下界说 WebConfig.java,它对应于 DispatcherServlet 装备。

@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.msdn.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/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

加载Spring容器

在 init 包下界说 Spring 容器初始化类 SpringApplicationInitializer,此类完结 WebApplicationInitializer 接口,Spring 容器启动时加载WebApplicationInitializer 接口的所有完结类。

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }
    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }
    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

SpringApplicationInitializer 相当于 web.xml,运用了servlet3.0开发则不需求再界说 web.xml,WebConfig.class 对应以下装备的 spring-mvc.xml,web.xml的内容参阅:

<web‐app>
<listener>
<listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐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>

完结认证功用

认证页面

在 webapp/WEB-INF/view 下界说认证页面 login.jsp,本事例只是测验认证流程,页面没有添加css样式,页面完结可填入用户名,暗码,触发登录将提交表单信息至/login,内容如下:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<p style="color: red">${msg }</p>
<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");
}

启动项目,装备 tomcat

基于Session的认证与授权实践

基于Session的认证与授权实践

认证接口

用户进入认证页面,输入账号和暗码,点击登录,恳求/login 进行身份认证。

1、界说认证接口,此接口用于对传来的用户名、暗码校验,若成功则回来该用户的详细信息,否则抛出过错反常:

public interface AuthenticationService {
  /**
   * 用户认证
   *
   * @param userRequest
   * @return
   */
  User authentication(UserRequest userRequest);
}

2、表单恳求参数封装为实体类

@Data
public class UserRequest {
  private String username;
  private String password;
}

3、认证成功后回来的用户详细信息

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
  private Long id;
  private String username;
  private String password;
  private String fullname;
  private String mobile;
}

4、认证服务详细完结类

@Service
public class AuthenticationServiceImpl implements AuthenticationService {
  private static Map<String, User> userMap = new HashMap<>();
  static {
    userMap.put("zhangsan", new UserDTO(1010L, "zhangsan", "123", "张三", "133443"));
    userMap.put("lisi", new UserDTO(1011L, "lisi", "456", "李四", "144553"));
  }
  @Override
  public User authentication(UserRequest userRequest) {
    User user = getUserByName(userRequest.getUsername());
    if (Objects.isNull(user)) {
      throw new RuntimeException("查询不到该用户");
    }
    if (!Objects.equals(user.getPassword(), userRequest.getPassword())) {
      throw new RuntimeException("账号或暗码过错");
    }
    return user;
  }
  /**
     * 仿照从表中依据用户名查询用户信息
     *
     * @param username
     * @return
     */
  public User getUserByName(String username) {
    return userMap.get(username);
  }
}

5、controller 对 login 恳求做处理

@RestController
public class LoginController {
  @Autowired
  private AuthenticationService authenticationService;
  @PostMapping(value = "/login")
  public String login(UserRequest request, Model model) {
    if (Objects.isNull(request) || isBlank(request.getUsername()) ||
        isBlank(request.getPassword())) {
      model.addAttribute("msg", "账号或暗码为空");
      return "login";
    }
    try {
      User user = authenticationService.authentication(request);
      return "redirect:hello";
    } catch (Exception e) {
      model.addAttribute("msg", e.getMessage());
    }
    return "login";
  }
}

6、测验,重新启动 tomcat

输入正确的用户名和暗码,则提示登录成功,如果账号或暗码不输入,则会提示报错信息;如果账号或暗码校验不通过,会提示详细报错。

完结会话功用

会话是指用户登入体系后,体系会记住该用户的登录状态,他能够在体系接连操作直到退出体系的进程。

认证的目的是对体系资源的维护,每次对资源的拜访,体系必须得知道是谁在拜访资源,才能对该恳求进行合法性拦截。因此,在认证成功后,一般会把认证成功的用户信息放入 Session中,在后续的恳求中,体系能够从 Session 中获取到当时用户,用这样的办法来完结会话机制。

在上一节咱们详细介绍了 Cookie 和 Session,咱们此处创立的项目启动后就作为暂时服务器,存储 session 信息,而客户端通常是将 sessionId 存放在 cookie 中的,所以咱们还需求设置 cookie 回来给客户端。

1、cookie 操作工具类

public class CookieUtil {
  public static Cookie addUserCookie(String cookieValue) {
    return addCookie("user_session_id", cookieValue);
  }
  public static Cookie addCookie(String cookieName, String cookieValue) {
    Cookie cookie = new Cookie(cookieName, cookieValue);
    cookie.setMaxAge(3600);
    cookie.setPath("/");
    return cookie;
  }
  public static String getUserCookie(HttpServletRequest request) {
    return getCookie(request, "user_session_id");
  }
  public static String getCookie(HttpServletRequest request, String cookieName) {
    Cookie[] cookies = request.getCookies();
    String cookieValue = "";
    for (Cookie cookie : cookies) {
      if (cookieName.equals(cookie.getName())) {
        cookieValue = cookie.getValue();
      }
    }
    return cookieValue;
  }
}

2、修正 controller 中 login 办法,当认证成功后,将用户信息放入当时会话,并将 sessionId 放入 cookie 中。并添加用户登出办法,登出时将 session 置为失效。

@PostMapping(value = "/login")
public String login(UserRequest request, HttpSession session, Model model,
                    HttpServletResponse response) {
  if (Objects.isNull(request) || isBlank(request.getUsername()) ||
      isBlank(request.getPassword())) {
    model.addAttribute("msg", "账号或暗码为空");
    return "login";
  }
  try {
    User user = authenticationService.authentication(request);
    String userSessionId = RandomUtil.getRandom().nextInt(10000) + "_user";
    session.setAttribute(userSessionId, user);
    Cookie cookie = CookieUtil.addUserCookie(userSessionId);
    response.addCookie(cookie);
    return "redirect:hello";
  } catch (Exception e) {
    model.addAttribute("msg", e.getMessage());
  }
  return "login";
}
@RequestMapping(value = "logout")
public String logout(HttpSession session) {
  session.invalidate();
  return "login";
}

3、在 controller 中添加资源拜访测验接口,判别 session 中是否有用户

  @RequestMapping(value = "/r/r1")
  public String r1(HttpServletRequest request, Model model) {
    String userSessionId = CookieUtil.getUserCookie(request);
    HttpSession session = request.getSession();
    User user = (User) session.getAttribute(userSessionId);
    String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
    model.addAttribute("text", fullName + " 拜访资源1");
    return "resource";
  }

4、重启 tomcat,未登录情况下直接拜访测验资源 r/r1,详细途径为:http://localhost:8080/r/r1

完结授权功用

现在咱们现已完结了用户身份凭证的校验以及登录的状态坚持,并且咱们也知道了怎么获取当时登录用户(从Session中获取)的信息,接下来,用户拜访体系需求经过授权,即需求完结如下功用:

  • 匿名用户(未登录用户)拜访拦截:制止匿名用户拜访某些资源。
  • 登录用户拜访拦截:依据用户的权限决议是否能拜访某些资源。

1、添加权限数据

实践工作中,用户和人物相关,然后人物又和权限表相关,在本次测验阶段,为了方便操作,咱们直接在 User 里添加权限属性。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
  private Long id;
  private String username;
  private String password;
  private String fullname;
  private String mobile;
  // 用户权限
  private Set<String> authorities;
}

2、并在 AuthenticationServiceImpl 认证服务详细完结类中给用户初始化权限,实践使用中肯定不会这样,会从数据库中获取用户信息。

  private static Map<String, User> userMap = new HashMap<>();
  static {
    Set<String> authoritie1 = new HashSet<>();
    authoritie1.add("p1");
    Set<String> authoritie2 = new HashSet<>();
    authoritie2.add("p2");
    userMap.put("zhangsan", new User(1010L, "zhangsan", "123", "张三", "133443", authoritie1));
    userMap.put("lisi", new User(1011L, "lisi", "456", "李四", "144553", authoritie2));
  }

3、添加测验资源

在 controller 文件中添加对资源 r1、r2 的拜访

@RequestMapping(value = "/r/r1")
public String r1(HttpServletRequest request, Model model) {
  String userSessionId = CookieUtil.getUserCookie(request);
  HttpSession session = request.getSession();
  User user = (User) session.getAttribute(userSessionId);
  String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
  model.addAttribute("text", fullName + " 拜访资源1");
  return "resource";
}
@RequestMapping(value = "/r/r2")
public String r2(HttpServletRequest request, Model model) {
  String userSessionId = CookieUtil.getUserCookie(request);
  HttpSession session = request.getSession();
  User user = (User) session.getAttribute(userSessionId);
  String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
  model.addAttribute("text", fullName + " 拜访资源2");
  return "resource";
}

4、完结授权拦截器

在 interceptor 包下界说 SimpleAuthenticationInterceptor 拦截器,完结授权拦截:

  1. 校验用户是否登录
  2. 校验用户是否拥有操作权限
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    String userSessionId = CookieUtil.getUserCookie(request);
    Object attribute = request.getSession().getAttribute(userSessionId);
    if (Objects.isNull(attribute)) {
      writeContent(response, "请先登录");
    }
    User user = (User) attribute;
    String requestURI = request.getRequestURI();
    if (user.getAuthorities().contains("p1") && requestURI.contains("r1")) {
      return true;
    }
    if (user.getAuthorities().contains("p2") && requestURI.contains("r2")) {
      return true;
    }
    if (requestURI.contains("resource")) {
      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();
  }
}

在 WebConfig 中装备拦截器,匹配 /r/**的资源为受维护的体系资源,拜访该资源的恳求进入 SimpleAuthenticationInterceptor 拦截器。

@Autowired
private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
}

4、重启 tomcat,张三登录后,别离拜访 r1 和 r2 资源,检查页面回来信息。

项目演示

1、登录

如果账号或暗码为空,点击登录按钮,则会提示“账号或暗码为空”。

基于Session的认证与授权实践

如果账号或暗码过错,点击登录按钮,页面展示如下:

基于Session的认证与授权实践

如果账号和暗码都正确,点击登录按钮,页面展示如下:

基于Session的认证与授权实践

登录成功后,咱们在浏览器上检查 cookie 中存储的 sessionId。

基于Session的认证与授权实践

2、资源拜访

张三能够拜访 r1 资源,但无权拜访 r2 资源。

基于Session的认证与授权实践

基于Session的认证与授权实践

李四能够拜访 r2 资源,但无权拜访 r1 资源。

小结

依据 session 的认证和授权办法比较简单,认证进程清晰明晰,但是在大型项目中修正费事,不易扩展。所以实践生产中咱们往往会考虑运用第三方安全结构(如 Spring Security,shiro等安全结构)来完结认证授权功用。

本文主要仍是对上一篇文章中提到的知识点进行实操,方便大家直观理解,关于登录认证还有其他操作,比如记住暗码等,这里就不过多介绍了。