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

  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.  建表并生成相应的实体类

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

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。除非不用懒加载。

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

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.  看效果

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

基于 Spring Security 的前后端分离的权限控制系统-LMLPHP

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);
06-20 05:38