2021-06-19

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

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

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

1.  引入maven依赖

 1 <? 2 <project "http://maven.apache.org/POM/4.0.0" "http://www.w3.org/2001/ 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=mysql10 spring.jpa.open-in-view=true11 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true12 spring.jpa.show-sql=true13 14 spring.redis.host=192.168.28.3115 spring.redis.port=637916 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 ChengJianSheng14  * @Date 2021/6/1215  */16 @Setter17 @Getter18 @Entity19 @Table(name = "sys_user")20 public class SysUserEntity implements Serializable {21 22  @Id23  @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  @OneToOne46  @JoinColumn(name = "dept_id")47  private SysDeptEntity dept;48 49  @ManyToMany50  @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 ChengJianSheng12  * @Date 2021/6/1213  */14 @Data15 @Entity16 @Table(name = "sys_dept")17 public class SysDeptEntity implements Serializable {18 19  @Id20  @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   * 父级部门ID32   */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 ChengJianSheng14  * @Date 2021/6/1215  */16 @Setter17 @Getter18 @Entity19 @Table(name = "sys_menu")20 public class SysMenuEntity implements Serializable {21 22  @Id23  @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   * 菜单/按钮URL41   */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   * 父级菜单ID53   */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 ChengJianSheng14  * @Date 2021/6/1215  */16 @Setter17 @Getter18 @Entity19 @Table(name = "sys_role")20 public class SysRoleEntity implements Serializable {21 22  @Id23  @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  @ManyToMany37  @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 // @ManyToMany43 // @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 ChengJianSheng14  * @Date 2021/6/1215  * @see User16  * @see org.springframework.security.core.userdetails.User17  */18 @Setter19 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  @Override35  public Collection<? extends GrantedAuthority> getAuthorities() {36   return authorities;37  }38 39  @Override40  public String getPassword() {41   return password;42  }43 44  @Override45  public String getUsername() {46   return username;47  }48 49  @Override50  public boolean isAccountNonExpired() {51   return true;52  }53 54  @Override55  public boolean isAccountNonLocked() {56   return true;57  }58 59  @Override60  public boolean isCredentialsNonExpired() {61   return true;62  }63 64  @Override65  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 ChengJianSheng21  * @Date 2021/6/1222  */23 @Service24 public class MyUserDetailsService implements UserDetailsService {25  @Resource26  private SysUserRepository sysUserRepository;27 28  @Override29  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 ChengJianSheng22  * @Date 2021/6/1223  */24 @Service25 public class MyUserDetailsService implements UserDetailsService {26  @Resource27  private SysUserRepository sysUserRepository;28 29  @Override30  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.faster; 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 @Component25 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {26 27  private static ObjectMapper objectMapper = new ObjectMapper();28 29  @Autowired30  private StringRedisTemplate stringRedisTemplate;31 32  @Override33  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.faster; 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 @Component19 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {20 21  private static ObjectMapper objectMapper = new ObjectMapper();22 23  @Override24  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.faster; 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 ChengJianSheng18  * @Date 2021/5/719  */20 @Component21 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {22 23  private static ObjectMapper objectMapper = new ObjectMapper();24 25  @Override26  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.faster; 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 @Component16 public class MyAccessDeniedHandler implements AccessDeniedHandler {17 18  private static ObjectMapper objectMapper = new ObjectMapper();19 20  @Override21  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.faster; 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  @Override18  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.faster; 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 @Component17 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {18 19  private static ObjectMapper objectMapper = new ObjectMapper();20 21  @Autowired22  private StringRedisTemplate stringRedisTemplate;23 24  @Override25  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 ChengJianSheng12  * @Date 2021/5/713  */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   * 生成Token21   * @param subject 用户名22   * @return23   */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 ChengJianSheng22  * @Date 2021/6/1723  */24 @Component25 public class TokenFilter extends OncePerRequestFilter {26 27  @Autowired28  private StringRedisTemplate stringRedisTemplate;29 30  @Override31  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      // 刷新token45      // 如果生存时间小于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 ChengJianSheng10  * @Date 2021/6/1211  */12 @RestController13 @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 ChengJianSheng19  * @Date 2021/6/1220  */21 @EnableGlobalMethodSecurity(prePostEnabled = true)22 @EnableWebSecurity23 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {24 25  @Autowired26  private MyUserDetailsService myUserDetailsService;27  @Autowired28  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;29  @Autowired30  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;31  @Autowired32  private TokenFilter tokenFilter;33 34  @Override35  protected void configure(AuthenticationManagerBuilder auth) throws Exception {36   auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());37  }38 39  @Override40  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 ChengJianSheng12  * @Date 2021/5/1213  */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  @Override37  public Object getCredentials() {38   return credentials;39  }40 41  @Override42  public Object getPrincipal() {43   return principal;44  }45 46  @Override47  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  @Override53  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 ChengJianSheng13  * @Date 2021/5/1214  */15 public class SmsAuthenticationProvider implements AuthenticationProvider {16 17  private MyUserDetailsService myUserDetailsService;18 19  @Override20  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  @Override64  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 ChengJianSheng17  * @Date 2021/5/1218  */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  @Override42  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 ChengJianSheng18  * @Date 2021/5/1219  */20 @Component21 public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {22 23  @Autowired24  private MyUserDetailsService myUserDetailsService;25  @Autowired26  private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;27  @Autowired28  private MyAuthenticationFailureHandler myAuthenticationFailureHandler;29 30  @Override31  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);

 









原文转载:http://www.shaoqun.com/a/815655.html

跨境电商:https://www.ikjzd.com/

口述:我和良家妇女火热的婚外关系(4/4):http://lady.shaoqun.com/m/a/38731.html

女同桌让我伸进她的裤子里 女同学让我尽情的玩弄她:http://lady.shaoqun.com/m/a/247246.html

坐在学长的紫色巨龙上写作业 学长我坚持不住了:http://www.30bags.com/m/a/249827.html

友家速递:https://www.ikjzd.com/w/1341


话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:权限如何加载权限匹配规则登录1.引入maven依赖1<?2<project"http://maven.apache.org/POM/4.0.0""http://www.w3.org/2001/3xsi:schemaLocation="http://maven.apache.org/POM
口述实录:怒杀"醋男"小舅子,姐夫那说不清楚的热心肠:http://lady.shaoqun.com/a/251024.html
口述实录:成了老男人的情感备胎 这桩爱情太狗血:http://lady.shaoqun.com/a/252848.html
岳让我扒她内裤 又紧又深又爽又湿又浪:http://lady.shaoqun.com/a/247918.html
甘孜州特有的香猪肉(图) - :http://www.30bags.com/a/407769.html
甘孜州有什么好玩的地方?:http://www.30bags.com/a/419453.html
赶海吃海鲜 做一次真正的三亚人:http://www.30bags.com/a/415544.html
丽江古城一日怎么玩,丽江古城一日路线安排 :http://www.30bags.com/a/436129.html
㖭我下面开车 在颠簸的路上一进一出:http://lady.shaoqun.com/a/248411.html
昨天被三个猛男弄个半死 女婿的那个很大:http://lady.shaoqun.com/a/283415.html
小说:激情似乎又在我心中燃烧:http://www.30bags.com/a/443891.html
湖南一13岁女孩被一15岁男孩性侵。家人打电话求助,警方立案调查。原告:一对夫妇,不起诉:http://www.30bags.com/a/443892.html
三对夫妇在旅行时发生性关系,被判刑后没有认罪:http://www.30bags.com/a/443893.html

No comments:

Post a Comment