話不多說,入正題。一個簡單的許可權控制系統需要考慮的問題如下:
- 許可權如何載入
- 許可權匹配規則
- 登入
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);