zoukankan      html  css  js  c++  java
  • 基于 Spring Security 的前后端分离的权限控制系统

    话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:

    1. 权限如何加载
    2. 权限匹配规则
    3. 登录

    1.  引入maven依赖

     1 <?xml version="1.0" encoding="UTF-8"?>
     2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     4     <modelVersion>4.0.0</modelVersion>
     5     <parent>
     6         <groupId>org.springframework.boot</groupId>
     7         <artifactId>spring-boot-starter-parent</artifactId>
     8         <version>2.5.1</version>
     9         <relativePath/> <!-- lookup parent from repository -->
    10     </parent>
    11     <groupId>com.example</groupId>
    12     <artifactId>demo5</artifactId>
    13     <version>0.0.1-SNAPSHOT</version>
    14     <name>demo5</name>
    15 
    16     <properties>
    17         <java.version>1.8</java.version>
    18     </properties>
    19 
    20     <dependencies>
    21         <dependency>
    22             <groupId>org.springframework.boot</groupId>
    23             <artifactId>spring-boot-starter-data-jpa</artifactId>
    24         </dependency>
    25         <dependency>
    26             <groupId>org.springframework.boot</groupId>
    27             <artifactId>spring-boot-starter-data-redis</artifactId>
    28         </dependency>
    29         <dependency>
    30             <groupId>org.springframework.boot</groupId>
    31             <artifactId>spring-boot-starter-security</artifactId>
    32         </dependency>
    33         <dependency>
    34             <groupId>org.springframework.boot</groupId>
    35             <artifactId>spring-boot-starter-web</artifactId>
    36         </dependency>
    37 
    38         <dependency>
    39             <groupId>io.jsonwebtoken</groupId>
    40             <artifactId>jjwt</artifactId>
    41             <version>0.9.1</version>
    42         </dependency>
    43 
    44         <dependency>
    45             <groupId>com.alibaba</groupId>
    46             <artifactId>fastjson</artifactId>
    47             <version>1.2.76</version>
    48         </dependency>
    49         <dependency>
    50             <groupId>org.apache.commons</groupId>
    51             <artifactId>commons-lang3</artifactId>
    52             <version>3.12.0</version>
    53         </dependency>
    54         <dependency>
    55             <groupId>commons-codec</groupId>
    56             <artifactId>commons-codec</artifactId>
    57             <version>1.15</version>
    58         </dependency>
    59 
    60         <dependency>
    61             <groupId>mysql</groupId>
    62             <artifactId>mysql-connector-java</artifactId>
    63             <scope>runtime</scope>
    64         </dependency>
    65         <dependency>
    66             <groupId>org.projectlombok</groupId>
    67             <artifactId>lombok</artifactId>
    68             <optional>true</optional>
    69         </dependency>
    70     </dependencies>
    71 
    72     <build>
    73         <plugins>
    74             <plugin>
    75                 <groupId>org.springframework.boot</groupId>
    76                 <artifactId>spring-boot-maven-plugin</artifactId>
    77                 <configuration>
    78                     <excludes>
    79                         <exclude>
    80                             <groupId>org.projectlombok</groupId>
    81                             <artifactId>lombok</artifactId>
    82                         </exclude>
    83                     </excludes>
    84                 </configuration>
    85             </plugin>
    86         </plugins>
    87     </build>
    88 
    89 </project>

    application.properties配置

     1 server.port=8080
     2 server.servlet.context-path=/demo
     3 
     4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
     5 spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8
     6 spring.datasource.username=root
     7 spring.datasource.password=123456
     8 
     9 spring.jpa.database=mysql
    10 spring.jpa.open-in-view=true
    11 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
    12 spring.jpa.show-sql=true
    13 
    14 spring.redis.host=192.168.28.31
    15 spring.redis.port=6379
    16 spring.redis.password=123456

    2.  建表并生成相应的实体类

    SysUser.java

     1 package com.example.demo5.entity;
     2 
     3 import lombok.Getter;
     4 import lombok.Setter;
     5 
     6 import javax.persistence.*;
     7 import java.io.Serializable;
     8 import java.time.LocalDate;
     9 import java.util.Set;
    10 
    11 /**
    12  * 用户表
    13  * @Author ChengJianSheng
    14  * @Date 2021/6/12
    15  */
    16 @Setter
    17 @Getter
    18 @Entity
    19 @Table(name = "sys_user")
    20 public class SysUserEntity implements Serializable {
    21 
    22     @Id
    23     @GeneratedValue(strategy = GenerationType.AUTO)
    24     @Column(name = "id")
    25     private Integer id;
    26 
    27     @Column(name = "username")
    28     private String username;
    29 
    30     @Column(name = "password")
    31     private String password;
    32 
    33     @Column(name = "mobile")
    34     private String mobile;
    35 
    36     @Column(name = "enabled")
    37     private Integer enabled;
    38 
    39     @Column(name = "create_time")
    40     private LocalDate createTime;
    41 
    42     @Column(name = "update_time")
    43     private LocalDate updateTime;
    44 
    45     @OneToOne
    46     @JoinColumn(name = "dept_id")
    47     private SysDeptEntity dept;
    48 
    49     @ManyToMany
    50     @JoinTable(name = "sys_user_role",
    51             joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
    52             inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
    53     private Set<SysRoleEntity> roles;
    54 
    55 }
    

    SysDept.java

    部门相当于用户组,这里简化了一下,用户组没有跟角色管理

     1 package com.example.demo5.entity;
     2 
     3 import lombok.Data;
     4 
     5 import javax.persistence.*;
     6 import java.io.Serializable;
     7 import java.util.Set;
     8 
     9 /**
    10  * 部门表
    11  * @Author ChengJianSheng
    12  * @Date 2021/6/12
    13  */
    14 @Data
    15 @Entity
    16 @Table(name = "sys_dept")
    17 public class SysDeptEntity implements Serializable {
    18 
    19     @Id
    20     @GeneratedValue(strategy = GenerationType.AUTO)
    21     @Column(name = "id")
    22     private Integer id;
    23 
    24     /**
    25      * 部门名称
    26      */
    27     @Column(name = "name")
    28     private String name;
    29 
    30     /**
    31      * 父级部门ID
    32      */
    33     @Column(name = "pid")
    34     private Integer pid;
    35 
    36 //    @ManyToMany(mappedBy = "depts")
    37 //    private Set<SysRoleEntity> roles;
    38 }

    SysMenu.java

    菜单相当于权限

     1 package com.example.demo5.entity;
     2 
     3 import lombok.Data;
     4 import lombok.Getter;
     5 import lombok.Setter;
     6 
     7 import javax.persistence.*;
     8 import java.io.Serializable;
     9 import java.util.Set;
    10 
    11 /**
    12  * 菜单表
    13  * @Author ChengJianSheng
    14  * @Date 2021/6/12
    15  */
    16 @Setter
    17 @Getter
    18 @Entity
    19 @Table(name = "sys_menu")
    20 public class SysMenuEntity implements Serializable {
    21 
    22     @Id
    23     @GeneratedValue(strategy = GenerationType.AUTO)
    24     @Column(name = "id")
    25     private Integer id;
    26 
    27     /**
    28      * 资源编码
    29      */
    30     @Column(name = "code")
    31     private String code;
    32 
    33     /**
    34      * 资源名称
    35      */
    36     @Column(name = "name")
    37     private String name;
    38 
    39     /**
    40      * 菜单/按钮URL
    41      */
    42     @Column(name = "url")
    43     private String url;
    44 
    45     /**
    46      * 资源类型(1:菜单,2:按钮)
    47      */
    48     @Column(name = "type")
    49     private Integer type;
    50 
    51     /**
    52      * 父级菜单ID
    53      */
    54     @Column(name = "pid")
    55     private Integer pid;
    56 
    57     /**
    58      * 排序号
    59      */
    60     @Column(name = "sort")
    61     private Integer sort;
    62 
    63     @ManyToMany(mappedBy = "menus")
    64     private Set<SysRoleEntity> roles;
    65 
    66 }

    SysRole.java

     1 package com.example.demo5.entity;
     2 
     3 import lombok.Data;
     4 import lombok.Getter;
     5 import lombok.Setter;
     6 
     7 import javax.persistence.*;
     8 import java.io.Serializable;
     9 import java.util.Set;
    10 
    11 /**
    12  * 角色表
    13  * @Author ChengJianSheng
    14  * @Date 2021/6/12
    15  */
    16 @Setter
    17 @Getter
    18 @Entity
    19 @Table(name = "sys_role")
    20 public class SysRoleEntity implements Serializable {
    21 
    22     @Id
    23     @GeneratedValue(strategy = GenerationType.AUTO)
    24     @Column(name = "id")
    25     private Integer id;
    26 
    27     /**
    28      * 角色名称
    29      */
    30     @Column(name = "name")
    31     private String name;
    32 
    33     @ManyToMany(mappedBy = "roles")
    34     private Set<SysUserEntity> users;
    35 
    36     @ManyToMany
    37     @JoinTable(name = "sys_role_menu",
    38             joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
    39             inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
    40     private Set<SysMenuEntity> menus;
    41 
    42 //    @ManyToMany
    43 //    @JoinTable(name = "sys_dept_role",
    44 //            joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
    45 //            inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")})
    46 //    private Set<SysDeptEntity> depts;
    47 
    48 }
    

    注意,不要使用@Data注解,因为@Data包含@ToString注解

    不要随便打印SysUser,例如:System.out.println(sysUser); 任何形式的toString()调用都不要有,否则很有可能造成循环调用,死递归。想想看,SysUser里面要查SysRole,SysRole要查SysMenu,SysMenu又要查SysRole。除非不用懒加载。

    3.  自定义UserDetails

    虽然可以使用Spring Security自带的User,但是笔者还是强烈建议自定义一个UserDetails,后面可以直接将其序列化成json缓存到redis中

     1 package com.example.demo5.domain;
     2 
     3 import lombok.Setter;
     4 import org.springframework.security.core.GrantedAuthority;
     5 import org.springframework.security.core.authority.SimpleGrantedAuthority;
     6 import org.springframework.security.core.userdetails.User;
     7 import org.springframework.security.core.userdetails.UserDetails;
     8 
     9 import java.util.Collection;
    10 import java.util.Set;
    11 
    12 /**
    13  * @Author ChengJianSheng
    14  * @Date 2021/6/12
    15  * @see User
    16  * @see org.springframework.security.core.userdetails.User
    17  */
    18 @Setter
    19 public class MyUserDetails implements UserDetails {
    20 
    21     private String username;
    22     private String password;
    23     private boolean enabled;
    24 //    private Collection<? extends GrantedAuthority> authorities;
    25     private Set<SimpleGrantedAuthority> authorities;
    26 
    27     public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {
    28         this.username = username;
    29         this.password = password;
    30         this.enabled = enabled;
    31         this.authorities = authorities;
    32     }
    33 
    34     @Override
    35     public Collection<? extends GrantedAuthority> getAuthorities() {
    36         return authorities;
    37     }
    38 
    39     @Override
    40     public String getPassword() {
    41         return password;
    42     }
    43 
    44     @Override
    45     public String getUsername() {
    46         return username;
    47     }
    48 
    49     @Override
    50     public boolean isAccountNonExpired() {
    51         return true;
    52     }
    53 
    54     @Override
    55     public boolean isAccountNonLocked() {
    56         return true;
    57     }
    58 
    59     @Override
    60     public boolean isCredentialsNonExpired() {
    61         return true;
    62     }
    63 
    64     @Override
    65     public boolean isEnabled() {
    66         return enabled;
    67     }
    68 }

     都自定义UserDetails了,当然要自己实现UserDetailsService了。这里当时偷懒直接用自带的User,后面放缓存的时候才知道不方便。

     1 package com.example.demo5.service;
     2 
     3 import com.example.demo5.entity.SysMenuEntity;
     4 import com.example.demo5.entity.SysRoleEntity;
     5 import com.example.demo5.entity.SysUserEntity;
     6 import com.example.demo5.repository.SysUserRepository;
     7 import org.apache.commons.lang3.StringUtils;
     8 import org.springframework.security.core.authority.SimpleGrantedAuthority;
     9 import org.springframework.security.core.userdetails.User;
    10 import org.springframework.security.core.userdetails.UserDetails;
    11 import org.springframework.security.core.userdetails.UserDetailsService;
    12 import org.springframework.security.core.userdetails.UsernameNotFoundException;
    13 import org.springframework.stereotype.Service;
    14 
    15 import javax.annotation.Resource;
    16 import java.util.Set;
    17 import java.util.stream.Collectors;
    18 
    19 /**
    20  * @Author ChengJianSheng
    21  * @Date 2021/6/12
    22  */
    23 @Service
    24 public class MyUserDetailsService implements UserDetailsService {
    25     @Resource
    26     private SysUserRepository sysUserRepository;
    27 
    28     @Override
    29     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    30         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
    31         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
    32         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
    33                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))
    34                 .map(SysMenuEntity::getCode)
    35                 .map(SimpleGrantedAuthority::new)
    36                 .collect(Collectors.toSet());
    37         User user = new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
    38         return user;
    39     }
    40 }
    

    算了,还是改过来吧

     1 package com.example.demo5.service;
     2 
     3 import com.example.demo5.domain.MyUserDetails;
     4 import com.example.demo5.entity.SysMenuEntity;
     5 import com.example.demo5.entity.SysRoleEntity;
     6 import com.example.demo5.entity.SysUserEntity;
     7 import com.example.demo5.repository.SysUserRepository;
     8 import org.apache.commons.lang3.StringUtils;
     9 import org.springframework.security.core.authority.SimpleGrantedAuthority;
    10 import org.springframework.security.core.userdetails.User;
    11 import org.springframework.security.core.userdetails.UserDetails;
    12 import org.springframework.security.core.userdetails.UserDetailsService;
    13 import org.springframework.security.core.userdetails.UsernameNotFoundException;
    14 import org.springframework.stereotype.Service;
    15 
    16 import javax.annotation.Resource;
    17 import java.util.Set;
    18 import java.util.stream.Collectors;
    19 
    20 /**
    21  * @Author ChengJianSheng
    22  * @Date 2021/6/12
    23  */
    24 @Service
    25 public class MyUserDetailsService implements UserDetailsService {
    26     @Resource
    27     private SysUserRepository sysUserRepository;
    28 
    29     @Override
    30     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    31         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
    32         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
    33         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
    34                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))
    35                 .map(SysMenuEntity::getCode)
    36                 .map(SimpleGrantedAuthority::new)
    37                 .collect(Collectors.toSet());
    38 //        return new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
    39         return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities);
    40     }
    41 }
    

    4.  自定义各种Handler

    登录成功

     1 package com.example.demo5.handler;
     2 
     3 import com.alibaba.fastjson.JSON;
     4 import com.example.demo5.domain.MyUserDetails;
     5 import com.example.demo5.domain.RespResult;
     6 import com.example.demo5.util.JwtUtils;
     7 import com.fasterxml.jackson.databind.ObjectMapper;
     8 import org.springframework.beans.factory.annotation.Autowired;
     9 import org.springframework.data.redis.core.StringRedisTemplate;
    10 import org.springframework.security.core.Authentication;
    11 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
    12 import org.springframework.stereotype.Component;
    13 
    14 import javax.servlet.ServletException;
    15 import javax.servlet.http.HttpServletRequest;
    16 import javax.servlet.http.HttpServletResponse;
    17 import java.io.IOException;
    18 import java.io.PrintWriter;
    19 import java.util.concurrent.TimeUnit;
    20 
    21 /**
    22  * 登录成功
    23  */
    24 @Component
    25 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    26 
    27     private static ObjectMapper objectMapper = new ObjectMapper();
    28 
    29     @Autowired
    30     private StringRedisTemplate stringRedisTemplate;
    31 
    32     @Override
    33     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
    34         MyUserDetails user = (MyUserDetails) authentication.getPrincipal();
    35         String username = user.getUsername();
    36         String token = JwtUtils.createToken(username);
    37         stringRedisTemplate.opsForValue().set("TOKEN:" + token, JSON.toJSONString(user), 60, TimeUnit.MINUTES);
    38 
    39         response.setContentType("application/json;charset=utf-8");
    40         PrintWriter writer = response.getWriter();
    41         writer.write(objectMapper.writeValueAsString(new RespResult<>(1, "success", token)));
    42         writer.flush();
    43         writer.close();
    44     }
    45 }

    登录失败

     1 package com.example.demo5.handler;
     2 
     3 import com.example.demo5.domain.RespResult;
     4 import com.fasterxml.jackson.databind.ObjectMapper;
     5 import org.springframework.security.core.AuthenticationException;
     6 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
     7 import org.springframework.stereotype.Component;
     8 
     9 import javax.servlet.ServletException;
    10 import javax.servlet.http.HttpServletRequest;
    11 import javax.servlet.http.HttpServletResponse;
    12 import java.io.IOException;
    13 import java.io.PrintWriter;
    14 
    15 /**
    16  * 登录失败
    17  */
    18 @Component
    19 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    20 
    21     private static ObjectMapper objectMapper = new ObjectMapper();
    22 
    23     @Override
    24     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    25         response.setContentType("application/json;charset=utf-8");
    26         PrintWriter writer = response.getWriter();
    27         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, exception.getMessage(), null)));
    28         writer.flush();
    29         writer.close();
    30     }
    31 }

    未登录

     1 package com.example.demo5.handler;
     2 
     3 import com.example.demo5.domain.RespResult;
     4 import com.fasterxml.jackson.databind.ObjectMapper;
     5 import org.springframework.security.core.AuthenticationException;
     6 import org.springframework.security.web.AuthenticationEntryPoint;
     7 import org.springframework.stereotype.Component;
     8 
     9 import javax.servlet.ServletException;
    10 import javax.servlet.http.HttpServletRequest;
    11 import javax.servlet.http.HttpServletResponse;
    12 import java.io.IOException;
    13 import java.io.PrintWriter;
    14 
    15 /**
    16  * 未认证(未登录)统一处理
    17  * @Author ChengJianSheng
    18  * @Date 2021/5/7
    19  */
    20 @Component
    21 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    22 
    23     private static ObjectMapper objectMapper = new ObjectMapper();
    24 
    25     @Override
    26     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    27         response.setContentType("application/json;charset=utf-8");
    28         PrintWriter writer = response.getWriter();
    29         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "未登录,请先登录", null)));
    30         writer.flush();
    31         writer.close();
    32     }
    33 }

    未授权

     1 package com.example.demo5.handler;
     2 
     3 import com.example.demo5.domain.RespResult;
     4 import com.fasterxml.jackson.databind.ObjectMapper;
     5 import org.springframework.security.access.AccessDeniedException;
     6 import org.springframework.security.web.access.AccessDeniedHandler;
     7 import org.springframework.stereotype.Component;
     8 
     9 import javax.servlet.ServletException;
    10 import javax.servlet.http.HttpServletRequest;
    11 import javax.servlet.http.HttpServletResponse;
    12 import java.io.IOException;
    13 import java.io.PrintWriter;
    14 
    15 @Component
    16 public class MyAccessDeniedHandler implements AccessDeniedHandler {
    17 
    18     private static ObjectMapper objectMapper = new ObjectMapper();
    19 
    20     @Override
    21     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    22         response.setContentType("application/json;charset=utf-8");
    23         PrintWriter writer = response.getWriter();
    24         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "抱歉,您没有权限访问", null)));
    25         writer.flush();
    26         writer.close();
    27     }
    28 }

    Session过期

     1 package com.example.demo5.handler;
     2 
     3 import com.example.demo5.domain.RespResult;
     4 import com.fasterxml.jackson.databind.ObjectMapper;
     5 import org.springframework.security.web.session.SessionInformationExpiredEvent;
     6 import org.springframework.security.web.session.SessionInformationExpiredStrategy;
     7 
     8 import javax.servlet.ServletException;
     9 import javax.servlet.http.HttpServletResponse;
    10 import java.io.IOException;
    11 import java.io.PrintWriter;
    12 
    13 public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
    14 
    15     private static ObjectMapper objectMapper = new ObjectMapper();
    16 
    17     @Override
    18     public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
    19         String msg = "登录超时或已在另一台机器登录,您被迫下线!";
    20         RespResult respResult = new RespResult(0, msg, null);
    21         HttpServletResponse response = event.getResponse();
    22         response.setContentType("application/json;charset=utf-8");
    23         PrintWriter writer = response.getWriter();
    24         writer.write(objectMapper.writeValueAsString(respResult));
    25         writer.flush();
    26         writer.close();
    27     }
    28 }

    退出成功

     1 package com.example.demo5.handler;
     2 
     3 import com.fasterxml.jackson.databind.ObjectMapper;
     4 import org.springframework.beans.factory.annotation.Autowired;
     5 import org.springframework.data.redis.core.StringRedisTemplate;
     6 import org.springframework.security.core.Authentication;
     7 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
     8 import org.springframework.stereotype.Component;
     9 
    10 import javax.servlet.ServletException;
    11 import javax.servlet.http.HttpServletRequest;
    12 import javax.servlet.http.HttpServletResponse;
    13 import java.io.IOException;
    14 import java.io.PrintWriter;
    15 
    16 @Component
    17 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    18 
    19     private static ObjectMapper objectMapper = new ObjectMapper();
    20 
    21     @Autowired
    22     private StringRedisTemplate stringRedisTemplate;
    23 
    24     @Override
    25     public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    26         String token = request.getHeader("token");
    27         stringRedisTemplate.delete("TOKEN:" + token);
    28 
    29         response.setContentType("application/json;charset=utf-8");
    30         PrintWriter printWriter = response.getWriter();
    31         printWriter.write(objectMapper.writeValueAsString("logout success"));
    32         printWriter.flush();
    33         printWriter.close();
    34     }
    35 }
    

    5.  Token处理

    现在由于前后端分离,服务端不再维持Session,于是需要token来作为访问凭证

    token工具类

     1 package com.example.demo5.util;
     2 
     3 import io.jsonwebtoken.*;
     4 
     5 import java.util.Date;
     6 import java.util.HashMap;
     7 import java.util.Map;
     8 import java.util.function.Function;
     9 
    10 /**
    11  * @Author ChengJianSheng
    12  * @Date 2021/5/7
    13  */
    14 public class JwtUtils {
    15 
    16     private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;
    17     private static String TOKEN_SECRET_KEY = "123456";
    18 
    19     /**
    20      * 生成Token
    21      * @param subject   用户名
    22      * @return
    23      */
    24     public static String createToken(String subject) {
    25         long currentTimeMillis = System.currentTimeMillis();
    26         Date currentDate = new Date(currentTimeMillis);
    27         Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);
    28 
    29         //  存放自定义属性,比如用户拥有的权限
    30         Map<String, Object> claims = new HashMap<>();
    31 
    32         return Jwts.builder()
    33                 .setClaims(claims)
    34                 .setSubject(subject)
    35                 .setIssuedAt(currentDate)
    36                 .setExpiration(expirationDate)
    37                 .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)
    38                 .compact();
    39     }
    40 
    41     public static String extractUsername(String token) {
    42         return extractClaim(token, Claims::getSubject);
    43     }
    44 
    45     public static boolean isTokenExpired(String token) {
    46         return extractExpiration(token).before(new Date());
    47     }
    48 
    49     public static Date extractExpiration(String token) {
    50         return extractClaim(token, Claims::getExpiration);
    51     }
    52 
    53     public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    54         final Claims claims = extractAllClaims(token);
    55         return claimsResolver.apply(claims);
    56     }
    57 
    58     private static Claims extractAllClaims(String token) {
    59         return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();
    60     }
    61 
    62 }

    前后端约定登录成功以后,将token放到header中。于是,我们需要过滤器来处理请求Header中的token,为此定义一个TokenFilter

     1 package com.example.demo5.filter;
     2 
     3 import com.alibaba.fastjson.JSON;
     4 import com.example.demo5.domain.MyUserDetails;
     5 import org.apache.commons.lang3.StringUtils;
     6 import org.springframework.beans.factory.annotation.Autowired;
     7 import org.springframework.data.redis.core.StringRedisTemplate;
     8 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
     9 import org.springframework.security.core.context.SecurityContextHolder;
    10 import org.springframework.stereotype.Component;
    11 import org.springframework.web.filter.OncePerRequestFilter;
    12 
    13 import javax.servlet.FilterChain;
    14 import javax.servlet.ServletException;
    15 import javax.servlet.http.HttpServletRequest;
    16 import javax.servlet.http.HttpServletResponse;
    17 import java.io.IOException;
    18 import java.util.concurrent.TimeUnit;
    19 
    20 /**
    21  * @Author ChengJianSheng
    22  * @Date 2021/6/17
    23  */
    24 @Component
    25 public class TokenFilter extends OncePerRequestFilter {
    26 
    27     @Autowired
    28     private StringRedisTemplate stringRedisTemplate;
    29 
    30     @Override
    31     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    32         String token = request.getHeader("token");
    33         System.out.println("请求头中带的token: " + token);
    34         String key = "TOKEN:" + token;
    35         if (StringUtils.isNotBlank(token)) {
    36             String value = stringRedisTemplate.opsForValue().get(key);
    37             if (StringUtils.isNotBlank(value)) {
    38 //                String username = JwtUtils.extractUsername(token);
    39                 MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);
    40                 if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {
    41                     UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    42                     SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    43 
    44                     //  刷新token
    45                     //  如果生存时间小于10分钟,则再续1小时
    46                     long time = stringRedisTemplate.getExpire(key);
    47                     if (time < 600) {
    48                         stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);
    49                     }
    50                 }
    51             }
    52         }
    53 
    54         chain.doFilter(request, response);
    55     }
    56 }

    token过滤器做了两件事,一是获取header中的token,构造UsernamePasswordAuthenticationToken放入上下文中。权限可以从数据库中再查一遍,也可以直接从之前的缓存中获取。二是为token续期,即刷新token。 

    由于我们采用jwt生成token,因此没法中途更改token的有效期,只能将其放到Redis中,通过更改Redis中key的生存时间来控制token的有效期。

    6.  访问控制

    首先来定义资源

     1 package com.example.demo5.controller;
     2 
     3 import org.springframework.security.access.prepost.PreAuthorize;
     4 import org.springframework.web.bind.annotation.GetMapping;
     5 import org.springframework.web.bind.annotation.RequestMapping;
     6 import org.springframework.web.bind.annotation.RestController;
     7 
     8 /**
     9  * @Author ChengJianSheng
    10  * @Date 2021/6/12
    11  */
    12 @RestController
    13 @RequestMapping("/hello")
    14 public class HelloController {
    15 
    16     @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')")
    17     @GetMapping("/sayHello")
    18     public String sayHello() {
    19         return "hello";
    20     }
    21 
    22     @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')")
    23     @GetMapping("/sayHi")
    24     public String sayHi() {
    25         return "hi";
    26     }
    27 }

    资源的访问控制我们通过判断是否有相应的权限字符串

     1 package com.example.demo5.service;
     2 
     3 import org.springframework.security.core.Authentication;
     4 import org.springframework.security.core.GrantedAuthority;
     5 import org.springframework.security.core.authority.SimpleGrantedAuthority;
     6 import org.springframework.security.core.context.SecurityContextHolder;
     7 import org.springframework.security.core.userdetails.UserDetails;
     8 import org.springframework.stereotype.Component;
     9 
    10 import java.util.Set;
    11 import java.util.stream.Collectors;
    12 
    13 @Component("myAccessDecisionService")
    14 public class MyAccessDecisionService {
    15 
    16     public boolean hasPermission(String permission) {
    17         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    18         Object principal = authentication.getPrincipal();
    19         if (principal instanceof UserDetails) {
    20             UserDetails userDetails = (UserDetails) principal;
    21 //            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
    22             Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
    23             return set.contains(permission);
    24         }
    25         return false;
    26     }
    27 }
    

    7.  配置WebSecurity

     1 package com.example.demo5.config;
     2 
     3 import com.example.demo5.filter.TokenFilter;
     4 import com.example.demo5.handler.*;
     5 import com.example.demo5.service.MyUserDetailsService;
     6 import org.springframework.beans.factory.annotation.Autowired;
     7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
     8 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
     9 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    10 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    11 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    12 import org.springframework.security.config.http.SessionCreationPolicy;
    13 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    14 import org.springframework.security.crypto.password.PasswordEncoder;
    15 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    16 
    17 /**
    18  * @Author ChengJianSheng
    19  * @Date 2021/6/12
    20  */
    21 @EnableGlobalMethodSecurity(prePostEnabled = true)
    22 @EnableWebSecurity
    23 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    24 
    25     @Autowired
    26     private MyUserDetailsService myUserDetailsService;
    27     @Autowired
    28     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    29     @Autowired
    30     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    31     @Autowired
    32     private TokenFilter tokenFilter;
    33 
    34     @Override
    35     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    36         auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    37     }
    38 
    39     @Override
    40     protected void configure(HttpSecurity http) throws Exception {
    41         http.formLogin()
    42 //                .usernameParameter("username")
    43 //                .passwordParameter("password")
    44 //                .loginPage("/login.html")
    45                 .successHandler(myAuthenticationSuccessHandler)
    46                 .failureHandler(myAuthenticationFailureHandler)
    47                 .and()
    48                 .logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
    49                 .and()
    50                 .authorizeRequests()
    51                 .antMatchers("/demo/login").permitAll()
    52 //                .antMatchers("/css/**", "/js/**", "/**/images/*.*").permitAll()
    53 //                .regexMatchers(".+[.]jpg").permitAll()
    54 //                .mvcMatchers("/hello").servletPath("/demo").permitAll()
    55                 .anyRequest().authenticated()
    56                 .and()
    57                 .exceptionHandling()
    58                 .accessDeniedHandler(new MyAccessDeniedHandler())
    59                 .authenticationEntryPoint(new MyAuthenticationEntryPoint())
    60                 .and()
    61                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    62                 .maximumSessions(1)
    63                 .maxSessionsPreventsLogin(false)
    64                 .expiredSessionStrategy(new MyExpiredSessionStrategy());
    65 
    66         http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
    67 
    68         http.csrf().disable();
    69     }
    70 
    71     public PasswordEncoder passwordEncoder() {
    72         return new BCryptPasswordEncoder();
    73     }
    74 
    75     public static void main(String[] args) {
    76         System.out.println(new BCryptPasswordEncoder().encode("123456"));
    77     }
    78 }

    注意,我们将自定义的TokenFilter放到UsernamePasswordAuthenticationFilter之前

    所有过滤器的顺序可以查看 org.springframework.security.config.annotation.web.builders.FilterComparator 或者 org.springframework.security.config.annotation.web.builders.FilterOrderRegistration

    8.  看效果

    9.  补充:手机号+短信验证码登录

    参照org.springframework.security.authentication.UsernamePasswordAuthenticationToken写一个短信认证Token

     1 package com.example.demo5.filter;
     2 
     3 import org.springframework.security.authentication.AbstractAuthenticationToken;
     4 import org.springframework.security.core.GrantedAuthority;
     5 import org.springframework.security.core.SpringSecurityCoreVersion;
     6 import org.springframework.util.Assert;
     7 
     8 import java.util.Collection;
     9 
    10 /**
    11  * @Author ChengJianSheng
    12  * @Date 2021/5/12
    13  */
    14 public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    15 
    16     private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    17 
    18     private final Object principal;
    19 
    20     private Object credentials;
    21 
    22     public SmsCodeAuthenticationToken(Object principal, Object credentials) {
    23         super(null);
    24         this.principal = principal;
    25         this.credentials = credentials;
    26         setAuthenticated(false);
    27     }
    28 
    29     public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
    30         super(authorities);
    31         this.principal = principal;
    32         this.credentials = credentials;
    33         super.setAuthenticated(true);
    34     }
    35 
    36     @Override
    37     public Object getCredentials() {
    38         return credentials;
    39     }
    40 
    41     @Override
    42     public Object getPrincipal() {
    43         return principal;
    44     }
    45 
    46     @Override
    47     public void setAuthenticated(boolean authenticated) {
    48         Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    49         super.setAuthenticated(false);
    50     }
    51 
    52     @Override
    53     public void eraseCredentials() {
    54         super.eraseCredentials();
    55     }
    56 }
    

    参照org.springframework.security.authentication.dao.DaoAuthenticationProvider写一个自己的短信认证Provider

     1 package com.example.demo5.filter;
     2 
     3 import com.example.demo.service.MyUserDetailsService;
     4 import org.apache.commons.lang3.StringUtils;
     5 import org.springframework.security.authentication.AuthenticationProvider;
     6 import org.springframework.security.authentication.BadCredentialsException;
     7 import org.springframework.security.core.Authentication;
     8 import org.springframework.security.core.AuthenticationException;
     9 import org.springframework.security.core.userdetails.UserDetails;
    10 
    11 /**
    12  * @Author ChengJianSheng
    13  * @Date 2021/5/12
    14  */
    15 public class SmsAuthenticationProvider implements AuthenticationProvider {
    16 
    17     private MyUserDetailsService myUserDetailsService;
    18 
    19     @Override
    20     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    21         //  校验验证码
    22         additionalAuthenticationChecks((SmsCodeAuthenticationToken) authentication);
    23 
    24         //  校验手机号
    25         String mobile = authentication.getPrincipal().toString();
    26 
    27         UserDetails userDetails = myUserDetailsService.loadUserByMobile(mobile);
    28 
    29         if (null == userDetails) {
    30             throw new BadCredentialsException("手机号不存在");
    31         }
    32 
    33         //  创建认证成功的Authentication对象
    34         SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
    35         result.setDetails(authentication.getDetails());
    36 
    37         return result;
    38     }
    39 
    40     protected void additionalAuthenticationChecks(SmsCodeAuthenticationToken authentication) throws AuthenticationException {
    41         if (authentication.getCredentials() == null) {
    42             throw new BadCredentialsException("验证码不能为空");
    43         }
    44         String mobile = authentication.getPrincipal().toString();
    45         String smsCode = authentication.getCredentials().toString();
    46 
    47         //  从Session或者Redis中获取相应的验证码
    48         String smsCodeInSessionKey = "SMS_CODE_" + mobile;
    49 //        String verificationCode = sessionStrategy.getAttribute(servletWebRequest, smsCodeInSessionKey);
    50 //        String verificationCode = stringRedisTemplate.opsForValue().get(smsCodeInSessionKey);
    51         String verificationCode = "1234";
    52 
    53         if (StringUtils.isBlank(verificationCode)) {
    54             throw new BadCredentialsException("短信验证码不存在,请重新发送!");
    55         }
    56         if (!smsCode.equalsIgnoreCase(verificationCode)) {
    57             throw new BadCredentialsException("验证码错误!");
    58         }
    59 
    60         //todo  清除Session或者Redis中获取相应的验证码
    61     }
    62 
    63     @Override
    64     public boolean supports(Class<?> authentication) {
    65         return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
    66     }
    67 
    68     public MyUserDetailsService getMyUserDetailsService() {
    69         return myUserDetailsService;
    70     }
    71 
    72     public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
    73         this.myUserDetailsService = myUserDetailsService;
    74     }
    75 }

    参照org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter写一个短信认证处理的过滤器

     1 package com.example.demo.filter;
     2 
     3 import org.springframework.security.authentication.AuthenticationManager;
     4 import org.springframework.security.authentication.AuthenticationServiceException;
     5 import org.springframework.security.core.Authentication;
     6 import org.springframework.security.core.AuthenticationException;
     7 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
     8 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
     9 
    10 import javax.servlet.ServletException;
    11 import javax.servlet.http.HttpServletRequest;
    12 import javax.servlet.http.HttpServletResponse;
    13 import java.io.IOException;
    14 
    15 /**
    16  * @Author ChengJianSheng
    17  * @Date 2021/5/12
    18  */
    19 public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    20 
    21     public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    22 
    23     public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "smsCode";
    24 
    25     private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login/mobile", "POST");
    26 
    27     private String usernameParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    28 
    29     private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    30 
    31     private boolean postOnly = true;
    32 
    33     public SmsAuthenticationFilter() {
    34         super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    35     }
    36 
    37     public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
    38         super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    39     }
    40 
    41     @Override
    42     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
    43         if (postOnly && !request.getMethod().equals("POST")) {
    44             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    45         }
    46 
    47         String mobile = obtainMobile(request);
    48         mobile = (mobile != null) ? mobile : "";
    49         mobile = mobile.trim();
    50         String smsCode = obtainPassword(request);
    51         smsCode = (smsCode != null) ? smsCode : "";
    52 
    53         SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);
    54 
    55         setDetails(request, authRequest);
    56 
    57         return this.getAuthenticationManager().authenticate(authRequest);
    58     }
    59 
    60     private String obtainMobile(HttpServletRequest request) {
    61         return request.getParameter(this.usernameParameter);
    62     }
    63 
    64     private String obtainPassword(HttpServletRequest request) {
    65         return request.getParameter(this.passwordParameter);
    66     }
    67 
    68     protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
    69         authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    70     }
    71 }

    在WebSecurity中进行配置

     1 package com.example.demo.config;
     2 
     3 import com.example.demo.filter.SmsAuthenticationFilter;
     4 import com.example.demo.filter.SmsAuthenticationProvider;
     5 import com.example.demo.handler.MyAuthenticationFailureHandler;
     6 import com.example.demo.handler.MyAuthenticationSuccessHandler;
     7 import com.example.demo.service.MyUserDetailsService;
     8 import org.springframework.beans.factory.annotation.Autowired;
     9 import org.springframework.security.authentication.AuthenticationManager;
    10 import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    11 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    12 import org.springframework.security.web.DefaultSecurityFilterChain;
    13 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    14 import org.springframework.stereotype.Component;
    15 
    16 /**
    17  * @Author ChengJianSheng
    18  * @Date 2021/5/12
    19  */
    20 @Component
    21 public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    22 
    23     @Autowired
    24     private MyUserDetailsService myUserDetailsService;
    25     @Autowired
    26     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    27     @Autowired
    28     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    29 
    30     @Override
    31     public void configure(HttpSecurity http) throws Exception {
    32         SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
    33         smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    34         smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
    35         smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
    36 
    37         SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
    38         smsAuthenticationProvider.setMyUserDetailsService(myUserDetailsService);
    39 
    40         http.authenticationProvider(smsAuthenticationProvider)
    41                 .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    42     }
    43 }
    
    1 http.apply(smsAuthenticationConfig);
    
  • 相关阅读:
    yum安装nginx没有某一模块,该如何添加第三方模块?
    二进制部署K8S集群(二十四)k8s技术点整理
    opencv在python和c#中的对照应用-文字区域识别
    小区配置地图中心坐标
    Exception: No supported Visual Studio can be found. Supported versions are: 16.0 (2019), 15.0 (2017)
    文件包含
    kali linux 安装lanmp遇到的问题
    嵌入式实操----基于RT1170 首板硬件之SDRAM调试(十二)
    嵌入式实操----基于RT1170 DCD数据开发入门记录(十一)
    嵌入式实操----基于RT1170解决串口不支持float类型打印问题(十)
  • 原文地址:https://www.cnblogs.com/cjsblog/p/14904861.html
Copyright © 2011-2022 走看看