软件编程
位置:首页>> 软件编程>> java编程>> 基于Spring Security前后端分离的权限控制系统问题

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

作者:废物大师兄  发布时间:2022-10-16 21:32:39 

标签:Spring,Security,前后端分离,权限控制

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

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

  1. 权限如何加载

  2. 权限匹配规则

  3. 登录

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

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

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

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

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

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

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

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

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

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

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

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

来源:https://www.cnblogs.com/cjsblog/p/14904861.html

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com