基於 Spring Security 的前後端分離的許可權控制系統

廢物大師兄發表於2021-06-19

話不多說,入正題。一個簡單的許可權控制系統需要考慮的問題如下:

  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);

 

相關文章