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

目录1. 引入maven依赖2. 建表并生成相应的实体类3. 自定义UserDetails4. 自定义各种Handler5. Token处理6. 访问控制7. 配置WebSecurity8. 看效果9. 补充:手机号+短信验证码登录

前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于Spring Security前后端分离的权限控制系统问题。

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

    权限如何加载 权限匹配规则 登录

1. 引入maven依赖

<?xml version="1.0" encoding="UTF-8"?>  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>     <parent>         <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.5.1</version>        <relativePath/> <!-- lookup parent from repository -->     </parent>     <groupId>com.example</groupId>     <artifactId>demo5</artifactId>     <version>0.0.1-SNAPSHOT</version>     <name>demo5</name>      <properties>         <java.version>1.8</java.version>     </properties>      <dependencies>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-jpa</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-redis</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-security</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>          <dependency>             <groupId>io.jsonwebtoken</groupId>             <artifactId>jjwt</artifactId>             <version>0.9.1</version>         </dependency>          <dependency>             <groupId>com.alibaba</groupId>             <artifactId>fastjson</artifactId>             <version>1.2.76</version>         </dependency>         <dependency>             <groupId>org.apache.commons</groupId>             <artifactId>commons-lang3</artifactId>             <version>3.12.0</version>         </dependency>         <dependency>             <groupId>commons-codec</groupId>             <artifactId>commons-codec</artifactId>             <version>1.15</version>         </dependency>          <dependency>             <groupId>mysql</groupId>             <artifactId>mysql-connector-java</artifactId>             <scope>runtime</scope>         </dependency>         <dependency>             <groupId>org.projectlombok</groupId>             <artifactId>lombok</artifactId>             <optional>true</optional>         </dependency>     </dependencies>      <build>         <plugins>             <plugin>                 <groupId>org.springframework.boot</groupId>                 <artifactId>spring-boot-maven-plugin</artifactId>                 <configuration>                     <excludes>                         <exclude>                             <groupId>org.projectlombok</groupId>                             <artifactId>lombok</artifactId>                         </exclude>                     </excludes>                 </configuration>             </plugin>         </plugins>     </build>  </project>

application.properties配置

server.port=8080 server.servlet.context-path=/demo  spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=123456 spring.jpa.database=mysql spring.jpa.open-in-view=true spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true spring.jpa.show-sql=true  spring.redis.host=192.168.28.31 spring.redis.port=6379 spring.redis.password=123456

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

SysUser.java

package com.example.demo5.entity;    import lombok.Getter;  import lombok.Setter;  import javax.persistence.*;  import java.io.Serializable;  import java.time.LocalDate;  import java.util.Set;  /**  * 用户表  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Setter @Getter @Entity @Table(name = "sys_user") public class SysUserEntity implements Serializable {      @Id     @GeneratedValue(strategy = GenerationType.AUTO)     @Column(name = "id")     private Integer id;      @Column(name = "username")     private String username;      @Column(name = "password")     private String password;      @Column(name = "mobile")     private String mobile;      @Column(name = "enabled")     private Integer enabled;      @Column(name = "create_time")     private LocalDate createTime;      @Column(name = "update_time")     private LocalDate updateTime;      @OneToOne     @JoinColumn(name = "dept_id")     private SysDeptEntity dept;      @ManyToMany     @JoinTable(name = "sys_user_role",             joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},             inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})     private Set<SysRoleEntity> roles;  }

SysDept.java

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

package com.example.demo5.entity;    import lombok.Data;    import javax.persistence.*;  import java.io.Serializable;  import java.util.Set;    /**  * 部门表  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Data @Entity @Table(name = "sys_dept") public class SysDeptEntity implements Serializable {      @Id     @GeneratedValue(strategy = GenerationType.AUTO)     @Column(name = "id")     private Integer id;      /**      * 部门名称      */     @Column(name = "name")     private String name;      /**      * 父级部门ID      */     @Column(name = "pid")     private Integer pid;  //    @ManyToMany(mappedBy = "depts") //    private Set<SysRoleEntity> roles; }

SysMenu.java

菜单相当于权限

package com.example.demo5.entity;    import lombok.Data;  import lombok.Getter;  import lombok.Setter;    import javax.persistence.*;  import java.io.Serializable; import java.util.Set;  /**  * 菜单表  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Setter @Getter @Entity @Table(name = "sys_menu") public class SysMenuEntity implements Serializable {      @Id     @GeneratedValue(strategy = GenerationType.AUTO)     @Column(name = "id")     private Integer id;      /**      * 资源编码      */     @Column(name = "code")     private String code;      /**      * 资源名称      */     @Column(name = "name")     private String name;      /**      * 菜单/按钮URL      */     @Column(name = "url")     private String url;      /**      * 资源类型(1:菜单,2:按钮)      */     @Column(name = "type")     private Integer type;      /**      * 父级菜单ID      */     @Column(name = "pid")     private Integer pid;      /**      * 排序号      */     @Column(name = "sort")     private Integer sort;      @ManyToMany(mappedBy = "menus")     private Set<SysRoleEntity> roles;  }

SysRole.java

package com.example.demo5.entity;    import lombok.Data;  import lombok.Getter;  import lombok.Setter;    import javax.persistence.*;  import java.io.Serializable;  import java.util.Set;  /**  * 角色表  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Setter @Getter @Entity @Table(name = "sys_role") public class SysRoleEntity implements Serializable {      @Id     @GeneratedValue(strategy = GenerationType.AUTO)     @Column(name = "id")     private Integer id;      /**      * 角色名称      */     @Column(name = "name")     private String name;      @ManyToMany(mappedBy = "roles")     private Set<SysUserEntity> users;      @ManyToMany     @JoinTable(name = "sys_role_menu",             joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},             inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})     private Set<SysMenuEntity> menus;  //    @ManyToMany //    @JoinTable(name = "sys_dept_role", //            joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}, //            inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")}) //    private Set<SysDeptEntity> depts;  }

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

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

3. 自定义UserDetails

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

package com.example.demo5.domain;    import lombok.Setter;  import org.springframework.security.core.GrantedAuthority;  import org.springframework.security.core.authority.SimpleGrantedAuthority;  import org.springframework.security.core.userdetails.User;  import org.springframework.security.core.userdetails.UserDetails;    import java.util.Collection; import java.util.Set;  /**  * @Author ChengJianSheng  * @Date 2021/6/12  * @see User  * @see org.springframework.security.core.userdetails.User  */ @Setter public class MyUserDetails implements UserDetails {      private String username;     private String password;     private boolean enabled; //    private Collection<? extends GrantedAuthority> authorities;     private Set<SimpleGrantedAuthority> authorities;      public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {         this.username = username;         this.password = password;         this.enabled = enabled;         this.authorities = authorities;     }      @Override     public Collection<? extends GrantedAuthority> getAuthorities() {         return authorities;     }      @Override     public String getPassword() {         return password;     }      @Override     public String getUsername() {         return username;     }      @Override     public boolean isAccountNonExpired() {         return true;     }      @Override     public boolean isAccountNonLocked() {         return true;     }      @Override     public boolean isCredentialsNonExpired() {         return true;     }      @Override     public boolean isEnabled() {         return enabled;     } }

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

package com.example.demo5.service;    import com.example.demo5.entity.SysMenuEntity;  import com.example.demo5.entity.SysRoleEntity;  import com.example.demo5.entity.SysUserEntity;  import com.example.demo5.repository.SysUserRepository;  import org.apache.commons.lang3.StringUtils;  import org.springframework.security.core.authority.SimpleGrantedAuthority;  import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;  import javax.annotation.Resource; import java.util.Set; import java.util.stream.Collectors;  /**  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Service public class MyUserDetailsService implements UserDetailsService {     @Resource     private SysUserRepository sysUserRepository;      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))                 .map(SysMenuEntity::getCode)                 .map(SimpleGrantedAuthority::new)                 .collect(Collectors.toSet());         User user = new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);         return user;     } }

算了,还是改过来吧

package com.example.demo5.service;    import com.example.demo5.domain.MyUserDetails;  import com.example.demo5.entity.SysMenuEntity;  import com.example.demo5.entity.SysRoleEntity;  import com.example.demo5.entity.SysUserEntity;  import com.example.demo5.repository.SysUserRepository;  import org.apache.commons.lang3.StringUtils;  import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;  import javax.annotation.Resource; import java.util.Set; import java.util.stream.Collectors;  /**  * @Author ChengJianSheng  * @Date 2021/6/12  */ @Service public class MyUserDetailsService implements UserDetailsService {     @Resource     private SysUserRepository sysUserRepository;      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))                 .map(SysMenuEntity::getCode)                 .map(SimpleGrantedAuthority::new)                 .collect(Collectors.toSet()); //        return new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);         return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities);     } }

4. 自定义各种Handler

登录成功

package com.example.demo5.handler;    import com.alibaba.fastjson.JSON;  import com.example.demo5.domain.MyUserDetails;  import com.example.demo5.domain.RespResult;  import com.example.demo5.util.JwtUtils;  import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component;  import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.concurrent.TimeUnit;  /**  * 登录成功  */ @Component public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {      private static ObjectMapper objectMapper = new ObjectMapper();      @Autowired     private StringRedisTemplate stringRedisTemplate;      @Override     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {         MyUserDetails user = (MyUserDetails) authentication.getPrincipal();         String username = user.getUsername();         String token = JwtUtils.createToken(username);         stringRedisTemplate.opsForValue().set("TOKEN:" + token, JSON.toJSONString(user), 60, TimeUnit.MINUTES);          response.setContentType("application/json;charset=utf-8");         PrintWriter writer = response.getWriter();         writer.write(objectMapper.writeValueAsString(new RespResult<>(1, "success", token)));         writer.flush();         writer.close();     } }

登录失败

package com.example.demo5.handler;    import com.example.demo5.domain.RespResult;  import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.security.core.AuthenticationException;  import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;  import org.springframework.stereotype.Component;    import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;  /**  * 登录失败  */ @Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {      private static ObjectMapper objectMapper = new ObjectMapper();      @Override     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {         response.setContentType("application/json;charset=utf-8");         PrintWriter writer = response.getWriter();         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, exception.getMessage(), null)));         writer.flush();         writer.close();     } }

未登录

package com.example.demo5.handler;    import com.example.demo5.domain.RespResult;  import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.security.core.AuthenticationException;  import org.springframework.security.web.AuthenticationEntryPoint;  import org.springframework.stereotype.Component;    import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;  /**  * 未认证(未登录)统一处理  * @Author ChengJianSheng  * @Date 2021/5/7  */ @Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {      private static ObjectMapper objectMapper = new ObjectMapper();      @Override     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {         response.setContentType("application/json;charset=utf-8");         PrintWriter writer = response.getWriter();         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "未登录,请先登录", null)));         writer.flush();         writer.close();     } }

未授权

package com.example.demo5.handler;    import com.example.demo5.domain.RespResult;  import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.security.access.AccessDeniedException;  import org.springframework.security.web.access.AccessDeniedHandler;  import org.springframework.stereotype.Component;    import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;  @Component public class MyAccessDeniedHandler implements AccessDeniedHandler {      private static ObjectMapper objectMapper = new ObjectMapper();      @Override     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {         response.setContentType("application/json;charset=utf-8");         PrintWriter writer = response.getWriter();         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "抱歉,您没有权限访问", null)));         writer.flush();         writer.close();     } }

Session过期

package com.example.demo5.handler;    import com.example.demo5.domain.RespResult;  import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.security.web.session.SessionInformationExpiredEvent;  import org.springframework.security.web.session.SessionInformationExpiredStrategy;    import javax.servlet.ServletException;  import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;  public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {      private static ObjectMapper objectMapper = new ObjectMapper();      @Override     public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {         String msg = "登录超时或已在另一台机器登录,您被迫下线!";         RespResult respResult = new RespResult(0, msg, null);         HttpServletResponse response = event.getResponse();         response.setContentType("application/json;charset=utf-8");         PrintWriter writer = response.getWriter();         writer.write(objectMapper.writeValueAsString(respResult));         writer.flush();         writer.close();     } }

退出成功

package com.example.demo5.handler;    import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.data.redis.core.StringRedisTemplate;  import org.springframework.security.core.Authentication;  import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;  import org.springframework.stereotype.Component;   import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter;  @Component public class MyLogoutSuccessHandler implements LogoutSuccessHandler {      private static ObjectMapper objectMapper = new ObjectMapper();      @Autowired     private StringRedisTemplate stringRedisTemplate;      @Override     public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {         String token = request.getHeader("token");         stringRedisTemplate.delete("TOKEN:" + token);          response.setContentType("application/json;charset=utf-8");         PrintWriter printWriter = response.getWriter();         printWriter.write(objectMapper.writeValueAsString("logout success"));         printWriter.flush();         printWriter.close();     } }

5. Token处理

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

token工具类

package com.example.demo5.util;    import io.jsonwebtoken.*;    import java.util.Date;  import java.util.HashMap;  import java.util.Map;  import java.util.function.Function;   /**  * @Author ChengJianSheng  * @Date 2021/5/7  */ public class JwtUtils {      private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;     private static String TOKEN_SECRET_KEY = "123456";      /**      * 生成Token      * @param subject   用户名      * @return      */     public static String createToken(String subject) {         long currentTimeMillis = System.currentTimeMillis();         Date currentDate = new Date(currentTimeMillis);         Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);          //  存放自定义属性,比如用户拥有的权限         Map<String, Object> claims = new HashMap<>();          return Jwts.builder()                 .setClaims(claims)                 .setSubject(subject)                 .setIssuedAt(currentDate)                 .setExpiration(expirationDate)                 .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)                 .compact();     }      public static String extractUsername(String token) {         return extractClaim(token, Claims::getSubject);     }      public static boolean isTokenExpired(String token) {         return extractExpiration(token).before(new Date());     }      public static Date extractExpiration(String token) {         return extractClaim(token, Claims::getExpiration);     }      public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {         final Claims claims = extractAllClaims(token);         return claimsResolver.apply(claims);     }      private static Claims extractAllClaims(String token) {         return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();     }  }

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

package com.example.demo5.filter;    import com.alibaba.fastjson.JSON;  import com.example.demo5.domain.MyUserDetails;  import org.apache.commons.lang3.StringUtils;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.data.redis.core.StringRedisTemplate;  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit;  /**  * @Author ChengJianSheng  * @Date 2021/6/17  */ @Component public class TokenFilter extends OncePerRequestFilter {      @Autowired     private StringRedisTemplate stringRedisTemplate;      @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {         String token = request.getHeader("token");         System.out.println("请求头中带的token: " + token);         String key = "TOKEN:" + token;         if (StringUtils.isNotBlank(token)) {             String value = stringRedisTemplate.opsForValue().get(key);             if (StringUtils.isNotBlank(value)) { //                String username = JwtUtils.extractUsername(token);                 MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);                 if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {                     UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());                     SecurityContextHolder.getContext().setAuthentication(authenticationToken);                      //  刷新token                     //  如果生存时间小于10分钟,则再续1小时                     long time = stringRedisTemplate.getExpire(key);                     if (time < 600) {                         stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);                     }                 }             }         }          chain.doFilter(request, response);     } }

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

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

6. 访问控制

首先来定义资源

package com.example.demo5.controller;    import org.springframework.security.access.prepost.PreAuthorize;  import org.springframework.web.bind.annotation.GetMapping;  import org.springframework.web.bind.annotation.RequestMapping;  import org.springframework.web.bind.annotation.RestController;    /**   * @Author ChengJianSheng  * @Date 2021/6/12  */ @RestController @RequestMapping("/hello") public class HelloController {      @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')")     @GetMapping("/sayHello")     public String sayHello() {         return "hello";     }      @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')")     @GetMapping("/sayHi")     public String sayHi() {         return "hi";     } }

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

package com.example.demo5.service;    import org.springframework.security.core.Authentication;  import org.springframework.security.core.GrantedAuthority;  import org.springframework.security.core.authority.SimpleGrantedAuthority;  import org.springframework.security.core.context.SecurityContextHolder;  import org.springframework.security.core.userdetails.UserDetails;  import org.springframework.stereotype.Component;   import java.util.Set; import java.util.stream.Collectors;  @Component("myAccessDecisionService") public class MyAccessDecisionService {      public boolean hasPermission(String permission) {         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();         Object principal = authentication.getPrincipal();         if (principal instanceof UserDetails) {             UserDetails userDetails = (UserDetails) principal; //            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);             Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());             return set.contains(permission);         }         return false;     } }

7. 配置WebSecurity

package com.example.demo5.config;    import com.example.demo5.filter.TokenFilter;  import com.example.demo5.handler.*;  import com.example.demo5.service.MyUserDetailsService;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;  import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;  import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  /**  * @Author ChengJianSheng  * @Date 2021/6/12  */ @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {      @Autowired     private MyUserDetailsService myUserDetailsService;     @Autowired     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;     @Autowired     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;     @Autowired     private TokenFilter tokenFilter;      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());     }      @Override     protected void configure(HttpSecurity http) throws Exception {         http.formLogin() //                .usernameParameter("username") //                .passwordParameter("password") //                .loginPage("/login.html")                 .successHandler(myAuthenticationSuccessHandler)                 .failureHandler(myAuthenticationFailureHandler)                 .and()                 .logout().logoutSuccessHandler(new MyLogoutSuccessHandler())                 .and()                 .authorizeRequests()                 .antMatchers("/demo/login").permitAll() //                .antMatchers("/css/**", "/js/**", "/**/images/*.*").permitAll() //                .regexMatchers(".+[.]jpg").permitAll() //                .mvcMatchers("/hello").servletPath("/demo").permitAll()                 .anyRequest().authenticated()                 .and()                 .exceptionHandling()                 .accessDeniedHandler(new MyAccessDeniedHandler())                 .authenticationEntryPoint(new MyAuthenticationEntryPoint())                 .and()                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                 .maximumSessions(1)                 .maxSessionsPreventsLogin(false)                 .expiredSessionStrategy(new MyExpiredSessionStrategy());          http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);          http.csrf().disable();     }      public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }      public static void main(String[] args) {         System.out.println(new BCryptPasswordEncoder().encode("123456"));     } }

注意,我们将自定义的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

package com.example.demo5.filter;    import org.springframework.security.authentication.AbstractAuthenticationToken;  import org.springframework.security.core.GrantedAuthority;  import org.springframework.security.core.SpringSecurityCoreVersion;  import org.springframework.util.Assert;    import java.util.Collection;   /**  * @Author ChengJianSheng  * @Date 2021/5/12  */ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {      private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;      private final Object principal;      private Object credentials;      public SmsCodeAuthenticationToken(Object principal, Object credentials) {         super(null);         this.principal = principal;         this.credentials = credentials;         setAuthenticated(false);     }      public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {         super(authorities);         this.principal = principal;         this.credentials = credentials;         super.setAuthenticated(true);     }      @Override     public Object getCredentials() {         return credentials;     }      @Override     public Object getPrincipal() {         return principal;     }      @Override     public void setAuthenticated(boolean authenticated) {         Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");         super.setAuthenticated(false);     }      @Override     public void eraseCredentials() {         super.eraseCredentials();     } }

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

package com.example.demo5.filter;   import com.example.demo.service.MyUserDetailsService;  import org.apache.commons.lang3.StringUtils;  import org.springframework.security.authentication.AuthenticationProvider;  import org.springframework.security.authentication.BadCredentialsException;  import org.springframework.security.core.Authentication;  import org.springframework.security.core.AuthenticationException;  import org.springframework.security.core.userdetails.UserDetails;  /**  * @Author ChengJianSheng  * @Date 2021/5/12  */ public class SmsAuthenticationProvider implements AuthenticationProvider {      private MyUserDetailsService myUserDetailsService;      @Override     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         //  校验验证码         additionalAuthenticationChecks((SmsCodeAuthenticationToken) authentication);          //  校验手机号         String mobile = authentication.getPrincipal().toString();          UserDetails userDetails = myUserDetailsService.loadUserByMobile(mobile);          if (null == userDetails) {             throw new BadCredentialsException("手机号不存在");         }          //  创建认证成功的Authentication对象         SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());         result.setDetails(authentication.getDetails());          return result;     }      protected void additionalAuthenticationChecks(SmsCodeAuthenticationToken authentication) throws AuthenticationException {         if (authentication.getCredentials() == null) {             throw new BadCredentialsException("验证码不能为空");         }         String mobile = authentication.getPrincipal().toString();         String smsCode = authentication.getCredentials().toString();          //  从Session或者Redis中获取相应的验证码         String smsCodeInSessionKey = "SMS_CODE_" + mobile; //        String verificationCode = sessionStrategy.getAttribute(servletWebRequest, smsCodeInSessionKey); //        String verificationCode = stringRedisTemplate.opsForValue().get(smsCodeInSessionKey);         String verificationCode = "1234";          if (StringUtils.isBlank(verificationCode)) {             throw new BadCredentialsException("短信验证码不存在,请重新发送!");         }         if (!smsCode.equalsIgnoreCase(verificationCode)) {             throw new BadCredentialsException("验证码错误!");         }          //todo  清除Session或者Redis中获取相应的验证码     }      @Override     public boolean supports(Class<?> authentication) {         return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));     }      public MyUserDetailsService getMyUserDetailsService() {         return myUserDetailsService;     }      public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {         this.myUserDetailsService = myUserDetailsService;     } }

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

package com.example.demo.filter;    import org.springframework.security.authentication.AuthenticationManager;  import org.springframework.security.authentication.AuthenticationServiceException;  import org.springframework.security.core.Authentication;  import org.springframework.security.core.AuthenticationException;  import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;   import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;  /**  * @Author ChengJianSheng  * @Date 2021/5/12  */ public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {      public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";      public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "smsCode";      private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login/mobile", "POST");      private String usernameParameter = SPRING_SECURITY_FORM_MOBILE_KEY;      private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;      private boolean postOnly = true;      public SmsAuthenticationFilter() {         super(DEFAULT_ANT_PATH_REQUEST_MATCHER);     }      public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {         super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);     }      @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {         if (postOnly && !request.getMethod().equals("POST")) {             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());         }          String mobile = obtainMobile(request);         mobile = (mobile != null) ? mobile : "";         mobile = mobile.trim();         String smsCode = obtainPassword(request);         smsCode = (smsCode != null) ? smsCode : "";          SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);          setDetails(request, authRequest);          return this.getAuthenticationManager().authenticate(authRequest);     }      private String obtainMobile(HttpServletRequest request) {         return request.getParameter(this.usernameParameter);     }      private String obtainPassword(HttpServletRequest request) {         return request.getParameter(this.passwordParameter);     }      protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {         authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));     } }

在WebSecurity中进行配置

package com.example.demo.config;    import com.example.demo.filter.SmsAuthenticationFilter;  import com.example.demo.filter.SmsAuthenticationProvider;  import com.example.demo.handler.MyAuthenticationFailureHandler;  import com.example.demo.handler.MyAuthenticationSuccessHandler;  import com.example.demo.service.MyUserDetailsService;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component;  /**  * @Author ChengJianSheng  * @Date 2021/5/12  */ @Component public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {      @Autowired     private MyUserDetailsService myUserDetailsService;     @Autowired     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;     @Autowired     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;      @Override     public void configure(HttpSecurity http) throws Exception {         SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();         smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));         smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);         smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);          SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();         smsAuthenticationProvider.setMyUserDetailsService(myUserDetailsService);          http.authenticationProvider(smsAuthenticationProvider)                 .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);     } } http.apply(smsAuthenticationConfig);

以上就是基于 Spring Security前后端分离的权限控制系统的详细内容,更多关于Spring Security权限控制系统的资料请关注其它相关文章!

黑夜下,撕开那张面具尽是怠倦的容颜,

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

相关文章:

你感兴趣的文章:

标签云: