数据库

数据库结构

共建五个表,分别用于存储权限信息、角色信息、权限角色对应关系、用户信息、用户角色对应关系。

以下为PostgreSQL建表过程:

-- 权限信息表
CREATE TABLE IF NOT EXISTS t_permission (  
    id serial PRIMARY KEY NOT NULL,  
    name varchar(255) NOT NULL,  
    url varchar(255) UNIQUE NOT NULL,  
    parent_id int  
);
-- 角色信息表
CREATE TABLE IF NOT EXISTS t_role (  
    id serial PRIMARY KEY NOT NULL ,  
    name varchar(50) UNIQUE NOT NULL DEFAULT '',  
    remark varchar(255) NULL default NULL  
);  
-- 角色、权限对应关系
CREATE TABLE IF NOT EXISTS t_role_permission (  
    id serial PRIMARY KEY NOT NULL ,  
    role_id int NOT NULL ,  
    permission_id int NOT NULL ,  
    UNIQUE (role_id, permission_id)  
);  
-- 角色、用户对应关系
CREATE TABLE IF NOT EXISTS t_user_role (  
    id serial PRIMARY KEY NOT NULL ,  
    role_id int NOT NULL ,  
    user_id int NOT NULL ,  
    UNIQUE (role_id, user_id)  
);  
-- 用户信息表
CREATE TABLE IF NOT EXISTS t_user (  
    id serial PRIMARY KEY NOT NULL ,  
    name varchar(255) NULL DEFAULT NULL,  
    username varchar(255) UNIQUE NOT NULL ,  
    password varchar(255) NOT NULL ,  
    phone varchar(255) NULL DEFAULT NULL,  
    gender bool NOT NULL DEFAULT true,  
    enable bool NOT NULL DEFAULT true,  
    last_login timestamp NULL DEFAULT NULL  
);

对于用户的权限,可以通过User -> Role -> Permission获取。

数据库初始化

对于系统初始化,可以通过以下的PostgreSQL进行:

INSERT INTO t_role (name, remark) VALUES ('超级管理员', '最高权限') ON CONFLICT DO NOTHING;  
INSERT INTO t_role (name, remark) VALUES ('系统管理员', '操作权限') ON CONFLICT DO NOTHING;  
  
INSERT INTO t_user (name, username, password, phone, gender, enable, last_login) VALUES ('超级管理员', 'sa', '123456', '131', true, true, current_timestamp) ON CONFLICT DO NOTHING;  
INSERT INTO t_user (name, username, password, phone, gender, enable, last_login) VALUES ('系统管理员', 'admin', '123456', '131', true, true, current_timestamp) ON CONFLICT DO NOTHING;  
  
INSERT INTO t_user_role (role_id, user_id) VALUES (1, 1) ON CONFLICT DO NOTHING;  
INSERT INTO t_user_role (role_id, user_id) VALUES (2, 2) ON CONFLICT DO NOTHING;  
  
INSERT INTO t_permission (name, url, parent_id) VALUES ('用户管理', '/api/user', 0) ON CONFLICT DO NOTHING;  
INSERT INTO t_permission (name, url, parent_id) VALUES ('系统管理', '/api/system', 0) ON CONFLICT DO NOTHING;  
INSERT INTO t_permission (name, url, parent_id) VALUES ('用户删除', '/api/user/delete', 1) ON CONFLICT DO NOTHING;  
  
INSERT INTO t_role_permission (role_id, permission_id) VALUES (1, 1) ON CONFLICT DO NOTHING;  
INSERT INTO t_role_permission (role_id, permission_id) VALUES (1, 2) ON CONFLICT DO NOTHING;  
INSERT INTO t_role_permission (role_id, permission_id) VALUES (1, 3) ON CONFLICT DO NOTHING;  
INSERT INTO t_role_permission (role_id, permission_id) VALUES (2, 2) ON CONFLICT DO NOTHING;

利用unique进行一次插入,后续不再插入;自动运行参考Spring项目#自动SQL执行

实体

以下实体省略settergetter全参构造器,实际开发请自己加上

数据库对应实体

创建com.example.webtest.entity.database包,所有数据库实体均放该包内。

用户实体

package com.example.webtest.entity.database;  
  
import com.baomidou.mybatisplus.annotation.TableName;  
import java.time.LocalDateTime;  
  
@TableName("t_user")  
public class User {  
    private Integer id;  
    private String name;  
    private String username;  
    private String password;  
    private String phone;  
    private Boolean gender;  
    private Boolean enable;  
    private LocalDateTime lastLogin;  
}

角色实体

package com.example.webtest.entity.database;  
import com.baomidou.mybatisplus.annotation.TableName;  
  
@TableName("t_role")  
public class Role {  
    private Integer id;  
    private String name;  
    private String remark;  
}

权限实体

package com.example.webtest.entity.database;  
import com.baomidou.mybatisplus.annotation.TableName;  
  
@TableName("t_permission")  
public class Permission {  
    private Integer id;  
    private String name;  
    private String url;  
    private Integer parentId;  
}

角色-权限对应关系实体

package com.example.webtest.entity.database;  
import com.baomidou.mybatisplus.annotation.TableName;  
  
@TableName("t_role_permission")  
public class RolePermission {  
    private Integer id;  
    private Integer roleId;  
    private Integer permissionId;  
}

用户-角色对应关系实体

package com.example.webtest.entity.database;  
import com.baomidou.mybatisplus.annotation.TableName;  
  
@TableName("t_user_role")  
public class UserRole {  
    private Integer id;  
    private Integer roleId;  
    private Integer userId;  
}

请求实体

创建`com.example.webtest.entity.request包,所有请求实体均放该包内。

用户实体【登录请求】

package com.example.webtest.entity.request;  
  
public class UserLoginDTO {  
    private String username;  
    private String password;  
}

响应实体

创建com.example.webtest.entity.response包,所有响应实体均放该包内。

API统一响应实体

package com.example.webtest.entity.response;  
import jakarta.servlet.http.HttpServletResponse;  
  
public class ResultDTO {  
    private Integer code;  
    private String message;  
    private Object data;  
    public static ResultDTO success() {  
        return success("ok");  
    }  
  
    public static ResultDTO success(String message) {  
        return success(message, null);  
    }  
  
    public static ResultDTO success(Object data) {  
        return success("ok", data);  
    }  
  
    public static ResultDTO success(String message, Object data) {  
        ResultDTO resultDTO = new ResultDTO();  
        resultDTO.setCode(HttpServletResponse.SC_OK);  
        resultDTO.setMessage(message);  
        resultDTO.setData(data);  
        return resultDTO;  
    }  
  
    public static ResultDTO error(String message) {  
        return error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);  
    }  
  
    public static ResultDTO error(int responseCode, Throwable e) {  
        return error(responseCode, e.getMessage() != null ? e.getMessage() : "System Error");  
    }  
  
    public static ResultDTO error(int responseCode, String message) {  
        ResultDTO resultDTO = new ResultDTO();  
        resultDTO.setCode(responseCode);  
        resultDTO.setMessage(message);  
        resultDTO.data = "";  
        return resultDTO;  
    }  
}

普通实体

创建com.example.webtest.entity.common包,所有普通实体均放该包内;此次实体大多是为内部准备。

用户账户实体

package com.example.webtest.entity.common;  
  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.userdetails.UserDetails;  
import java.util.Collection;  
  
public class AccountUser implements UserDetails {  
    private final Integer userId;  
    private final String password;  
    private final String username;  
    private final Collection<? extends GrantedAuthority> authorities;  
    private final boolean accountNonExpired;  
    private final boolean accountNonLocked;  
    private final boolean credentialsNonExpired;  
    private final boolean enabled;  
  
    public AccountUser(Integer userId, String password, String username, Collection<? extends GrantedAuthority> authorities) {  
        this(userId, password, username, authorities, true, true, true, true);  
    }  
  
    public AccountUser(Integer userId, String password, String username, Collection<? extends GrantedAuthority> authorities, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {  
        this.userId = userId;  
        this.password = password;  
        this.username = username;  
        this.authorities = authorities;  
        this.accountNonExpired = accountNonExpired;  
        this.accountNonLocked = accountNonLocked;  
        this.credentialsNonExpired = credentialsNonExpired;  
        this.enabled = enabled;  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
        return this.authorities;  
    }  
  
    @Override  
    public String getPassword() {  
        return this.password;  
    }  
  
    @Override  
    public String getUsername() {  
        return this.username;  
    }  
  
    @Override  
    public boolean isAccountNonExpired() {  
        return this.accountNonExpired;  
    }  
  
    @Override  
    public boolean isAccountNonLocked() {  
        return this.accountNonLocked;  
    }  
  
    @Override  
    public boolean isCredentialsNonExpired() {  
        return this.credentialsNonExpired;  
    }  
  
    @Override  
    public boolean isEnabled() {  
        return this.enabled;  
    }  
}

Mapper

用户实体Mapper

package com.example.webtest.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.example.webtest.entity.database.User;  
import org.apache.ibatis.annotations.Mapper;  
  
@Mapper  
public interface UserMapper extends BaseMapper<User> {  
}

角色实体Mapper

package com.example.webtest.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.example.webtest.entity.database.Role;  
import org.apache.ibatis.annotations.Mapper;  
  
@Mapper  
public interface RoleMapper extends BaseMapper<Role> {  
}

权限实体Mapper

package com.example.webtest.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.example.webtest.entity.database.Permission;  
import org.apache.ibatis.annotations.Mapper;  
  
@Mapper  
public interface PermissionMapper extends BaseMapper<Permission> {  
}

角色-权限对应关系实体Mapper

package com.example.webtest.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.example.webtest.entity.database.RolePermission;  
import org.apache.ibatis.annotations.Mapper;  
  
@Mapper  
public interface RolePermissionMapper extends BaseMapper<RolePermission> {  
}

用户-角色对应关系实体Mapper

package com.example.webtest.mapper;  
  
import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
import com.example.webtest.entity.database.UserRole;  
import org.apache.ibatis.annotations.Mapper;  
  
@Mapper  
public interface UserRoleMapper extends BaseMapper<UserRole> {  
}

Service

数据库相关Service

创建com.example.webtest.service.database包,所有数据库相关Service均放该包内。

用户Service

package com.example.webtest.service;  
  
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;  
import com.baomidou.mybatisplus.core.toolkit.Wrappers;  
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;  
import com.example.webtest.entity.database.Permission;  
import com.example.webtest.entity.database.RolePermission;  
import com.example.webtest.entity.database.User;  
import com.example.webtest.entity.database.UserRole;  
import com.example.webtest.mapper.UserMapper;  
import org.springframework.stereotype.Service;  
import java.util.ArrayList;  
import java.util.List;  
  
@Service  
public class UserService extends ServiceImpl<UserMapper, User> {  
  
    private final UserRoleService userRoleService;  
    private final RolePermissionService rolePermissionService;  
    private final PermissionService permissionService;  
  
    public UserService(UserRoleService userRoleService, RolePermissionService rolePermissionService, PermissionService permissionService) {  
        this.userRoleService = userRoleService;  
        this.rolePermissionService = rolePermissionService;  
        this.permissionService = permissionService;  
    }  
  
    public List<Permission> getPermissionByUsername(String username) {  
        User user = super.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username), true);  
        return this.getPermissionByUser(user);  
    }  
  
    public List<Permission> getPermissionByUser(User user) {  
        List<Permission> permissions = new ArrayList<>();  
        if (user != null) {  
            List<UserRole> userRoles = userRoleService.list(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId()));  
            if (CollectionUtils.isNotEmpty(userRoles)) {  
                List<Integer> roleIds = new ArrayList<>();  
                userRoles.stream().forEach(userRole -> {  
                    roleIds.add(userRole.getRoleId());  
                });  
                List<RolePermission> rolePermissions = rolePermissionService.list(Wrappers.<RolePermission>lambdaQuery().in(RolePermission::getRoleId, roleIds));  
                if (CollectionUtils.isNotEmpty(rolePermissions)) {  
                    List<Integer> permissionIds = new ArrayList<>();  
                    rolePermissions.stream().forEach(rolePermission -> {  
                        permissionIds.add(rolePermission.getPermissionId());  
                    });  
                    permissions = permissionService.list(Wrappers.<Permission>lambdaQuery().in(Permission::getId, permissionIds));  
                }  
            }  
        }  
        return permissions;  
    }  
}

角色Service

package com.example.webtest.service;  
  
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;  
import com.example.webtest.entity.database.Role;  
import com.example.webtest.mapper.RoleMapper;  
import org.springframework.stereotype.Service;  
  
@Service  
public class RoleService extends ServiceImpl<RoleMapper, Role> {  
}

权限Service

package com.example.webtest.service;  
  
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;  
import com.example.webtest.entity.database.Permission;  
import com.example.webtest.mapper.PermissionMapper;  
import org.springframework.stereotype.Service;  
  
@Service  
public class PermissionService extends ServiceImpl<PermissionMapper, Permission> {  
}

角色-权限对应关系Service

package com.example.webtest.service;  
  
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;  
import com.example.webtest.entity.database.RolePermission;  
import com.example.webtest.mapper.RolePermissionMapper;  
import org.springframework.stereotype.Service;  
  
@Service  
public class RolePermissionService extends ServiceImpl<RolePermissionMapper, RolePermission> {  
}

用户-角色对应关系Service

package com.example.webtest.service;  
  
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;  
import com.example.webtest.entity.database.UserRole;  
import com.example.webtest.mapper.UserRoleMapper;  
import org.springframework.stereotype.Service;  
  
@Service  
public class UserRoleService extends ServiceImpl<UserRoleMapper, UserRole> {  
}

普通Service

创建com.example.webtest.service.common包,所有普通Service均放该包内。

用户账户细节Service

package com.example.webtest.service.common;  
  
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;  
import com.baomidou.mybatisplus.core.toolkit.Wrappers;  
import com.example.webtest.entity.common.AccountUser;  
import com.example.webtest.entity.database.Permission;  
import com.example.webtest.service.database.UserService;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.core.authority.AuthorityUtils;  
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 com.example.webtest.entity.database.User;  
import java.util.List;  
  
@Service  
public class AccountUserDetailService implements UserDetailsService {  
    private final UserService userService;  
  
    public AccountUserDetailService(UserService userService) {  
        this.userService = userService;  
    }  
  
    @Override  
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
        User user = userService.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username), true);  
        if (user == null) {  
            throw new UsernameNotFoundException("Wrong username or password");  
        }  
        return new AccountUser(user.getId(), user.getUsername(), user.getPassword(), this.getUserAuthority(username));  
    }  
  
    public List<GrantedAuthority> getUserAuthority(String username) {  
        List<Permission> permissions = userService.getPermissionByUsername(username);  
        String authority = "";  
        if (CollectionUtils.isNotEmpty(permissions)) {  
            List<String> urls = permissions.stream().map(Permission::getUrl).toList();  
            authority = String.join(",", urls);  
        }  
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);  
    }  
}

JWT 认证

JWT 工具类

package com.example.webtest.util;  
  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.security.Keys;  
import org.springframework.stereotype.Component;  
import javax.crypto.SecretKey;  
import java.nio.charset.StandardCharsets;  
import java.sql.Timestamp;  
import java.time.LocalDateTime;  
import java.util.*;  
  
@Component  
public class JwtUtil {  
    private static String SECRET = null;  
    private static final long EXPIRE = 60 * 24 * 7;  
    public static final String HEADER = "Authorization";  
  
    static {  
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";  
        Random random = new Random();  
        StringBuilder stringBuilder = new StringBuilder();  
        for (int i = 0; i < 120; i++) {  
            stringBuilder.append(str.charAt(random.nextInt(str.length())));  
        }  
        SECRET = stringBuilder.toString();  
    }  
  
    public String generateToken(String username, List<String> url_prefix) {  
        SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));  
        LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE);  
        Map<String, Object> detail = new HashMap<>();  
        detail.put("username", username);  
        detail.put("permission", url_prefix);  
        return Jwts.builder()  
                .signWith(signingKey, Jwts.SIG.HS512)  
                .header().add("type", "JWT").and()  
                .issuedAt(Timestamp.valueOf(LocalDateTime.now()))  
                .subject(username)  
                .expiration(Timestamp.valueOf(tokenExpirationTime))  
                .claims(detail)  
                .compact();  
    }  
  
    public Claims getClaimsByToken(String token) {  
        SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));  
        try {  
            return Jwts.parser()  
                    .verifyWith(signingKey)  
                    .build()  
                    .parseSignedClaims(token)  
                    .getPayload();  
        } catch (Exception e) {  
            return null;  
        }  
    }  
  
    public boolean isTokenExpired(Date expiration) {  
        return expiration.before(new Date());  
    }  
  
}

根据相关信息生成一个JWT或者检验JWT是否过期;从JWT中获取Claims

JWT认证相关Handler

创建com.example.webtest.handler.jwt包,所有的JWT-Handler均放该包内。

AccessDeniedHandler

package com.example.webtest.handler.jwt;  
  
import com.example.webtest.entity.response.ResultDTO;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.ServletOutputStream;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import org.springframework.security.access.AccessDeniedException;  
import org.springframework.security.web.access.AccessDeniedHandler;  
import org.springframework.stereotype.Component;  
  
import java.io.IOException;  
  
@Component  
public class JwtAccessDeniedHandler implements AccessDeniedHandler {  
  
    @Override  
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {  
        response.setContentType("application/json;charset=UTF-8");  
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);  
        ResultDTO resultDTO = ResultDTO.error(accessDeniedException.getMessage());  
        ServletOutputStream outputStream = response.getOutputStream();  
        outputStream.write(new ObjectMapper().writeValueAsBytes(resultDTO));  
        outputStream.flush();  
        outputStream.close();  
    }  
}

AuthenticationEntryPoint

package com.example.webtest.handler.jwt;  
  
import com.example.webtest.entity.response.ResultDTO;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import jakarta.servlet.ServletOutputStream;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import org.springframework.security.core.AuthenticationException;  
import org.springframework.security.web.AuthenticationEntryPoint;  
import org.springframework.stereotype.Component;  
  
import java.io.IOException;  
  
@Component  
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {  
    @Override  
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {  
        response.setContentType("application/json;charset=UTF-8");  
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  
        ResultDTO resultDTO = ResultDTO.error(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");  
        ServletOutputStream outputStream = response.getOutputStream();  
        outputStream.write(new ObjectMapper().writeValueAsBytes(resultDTO));  
        outputStream.flush();  
        outputStream.close();  
    }  
}

LogoutSuccessHandler

package com.example.webtest.handler.jwt;  
  
import com.example.webtest.util.JwtUtil;  
import com.example.webtest.entity.response.ResultDTO;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.ServletOutputStream;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;  
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;  
import org.springframework.stereotype.Component;  
  
import java.io.IOException;  
  
@Component  
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {  
    @Override  
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {  
        if (authentication != null) {  
            new SecurityContextLogoutHandler().logout(request, response, authentication);  
        }  
        response.setContentType("application/json;charset=UTF-8");  
        response.setHeader(JwtUtil.HEADER, "");  
        SecurityContextHolder.clearContext();  
  
        ResultDTO resultDTO = ResultDTO.success("Logout success");  
        ServletOutputStream outputStream = response.getOutputStream();  
        outputStream.write(new ObjectMapper().writeValueAsBytes(resultDTO));  
        outputStream.flush();  
        outputStream.close();  
    }  
}

JwtAuthenticationFilter

package com.example.webtest.filter;  
  
import com.example.webtest.exception.BaseException;  
import com.example.webtest.util.JwtUtil;  
import io.jsonwebtoken.Claims;  
import jakarta.annotation.Resource;  
import jakarta.servlet.FilterChain;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import org.apache.logging.log4j.util.Strings;  
import org.springframework.beans.factory.annotation.Qualifier;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.authority.AuthorityUtils;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;  
import org.springframework.stereotype.Component;  
import org.springframework.web.filter.OncePerRequestFilter;  
import org.springframework.web.servlet.HandlerExceptionResolver;  
import java.io.IOException;  
import java.util.List;  
  
@Component  
public class JwtAuthenticationFilter extends OncePerRequestFilter {  
    @Resource  
    private JwtUtil jwtUtil;  
  
    private final HandlerExceptionResolver resolver;  
  
    public JwtAuthenticationFilter(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {  
        this.resolver = resolver;  
    }  
  
    @Override  
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {  
        String token = request.getHeader(JwtUtil.HEADER);  
        if (Strings.isBlank(token)) {  
            filterChain.doFilter(request, response);  
            return ;  
        }  
        Claims claims = jwtUtil.getClaimsByToken(token);  
        if (claims == null) {  
            resolver.resolveException(request, response, null, new BaseException(HttpServletResponse.SC_UNAUTHORIZED, "token invalid"));  
            return ;  
        }  
        if (jwtUtil.isTokenExpired(claims.getExpiration())) {  
            resolver.resolveException(request, response, null, new BaseException(HttpServletResponse.SC_UNAUTHORIZED, "token expired"));  
            return ;  
        }  
        String username = claims.getSubject();  
        List<String> url_prefix = (List<String>) claims.get("permission");  
        boolean unauthorized = true;  
        for (String prefix : url_prefix) {  
            if (request.getRequestURI().startsWith(prefix)){  
                unauthorized = false;  
                break;  
            }  
        }  
        if (unauthorized) {  
            resolver.resolveException(request, response, null, new BaseException(HttpServletResponse.SC_UNAUTHORIZED, "insufficient permissions"));  
            return ;  
        }  
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", url_prefix)));  
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));  
        SecurityContextHolder.getContext().setAuthentication(authentication);  
        filterChain.doFilter(request, response);  
    }  
}

注意此处如果需要转入到自定义的异常处理,则必须使用resolver.resolveException,否则会进入到JwtAuthenticationEntryPoint进行处理。并且使用resolver.resolveException需要先按照Spring项目#全局异常处理注册全局异常处理。 此外,这里实际将权限信息写入了JWT,并且在这里也通过了JWT进行权限验证,好处是这样就不会反复请求数据库了;缺点是一旦数据库更新,权限关系很可能就无法再对应了;可根据实际场景修改。

Spring Security配置

package com.example.webtest.config;  
  
import com.example.webtest.filter.JwtAuthenticationFilter;  
import com.example.webtest.handler.jwt.JwtAccessDeniedHandler;  
import com.example.webtest.handler.jwt.JwtAuthenticationEntryPoint;  
import com.example.webtest.handler.jwt.JwtLogoutSuccessHandler;  
import com.example.webtest.service.common.AccountUserDetailService;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.authentication.AuthenticationProvider;  
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;  
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;  
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.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.web.SecurityFilterChain;  
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  
  
@Configuration  
@EnableWebSecurity  
public class SecurityConfig {  
    private static final String[] URL_WHITELIST = {"/user/login", "/user/logout", "/favicon.ico"};  
  
    private final AccountUserDetailService accountUserDetailService;  
    private final JwtAuthenticationFilter jwtAuthenticationFilter;  
    private final JwtLogoutSuccessHandler jwtLogoutSuccessHandler;  
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;  
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;  
  
    public SecurityConfig(AccountUserDetailService accountUserDetailService, JwtAuthenticationFilter jwtAuthenticationFilter, JwtLogoutSuccessHandler jwtLogoutSuccessHandler, JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) {  
        this.accountUserDetailService = accountUserDetailService;  
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;  
        this.jwtLogoutSuccessHandler = jwtLogoutSuccessHandler;  
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;  
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;  
    }  
  
    @Bean  
    public AuthenticationProvider authenticationProvider() {  
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();  
        authenticationProvider.setUserDetailsService(accountUserDetailService);  
        return authenticationProvider;  
    }  
  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{  
        return authenticationConfiguration.getAuthenticationManager();  
    }  
  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {  
        httpSecurity  
                .csrf(AbstractHttpConfigurer::disable)  
                .logout(logout -> logout.logoutSuccessHandler(jwtLogoutSuccessHandler))  
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
                .authorizeHttpRequests(auth -> auth.requestMatchers(URL_WHITELIST).permitAll())  
                .authorizeHttpRequests(auth -> auth.requestMatchers(new String[]{"/static/**"}).permitAll())  
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())  
                .exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler))  
                .authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);  
        return httpSecurity.build();  
    }  
}

Controller

仅用于测试

package com.example.webtest.controller;  
  
import com.baomidou.mybatisplus.core.toolkit.Wrappers;  
import com.example.webtest.util.JwtUtil;  
import com.example.webtest.entity.response.ResultDTO;  
import com.example.webtest.entity.request.UserLoginDTO;  
import com.example.webtest.entity.database.User;  
import com.example.webtest.service.database.UserService;  
import jakarta.annotation.Resource;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import org.springframework.security.core.Authentication;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;  
import org.springframework.validation.annotation.Validated;  
import org.springframework.web.bind.annotation.*;  
  
import java.time.LocalDateTime;  
import java.util.*;  
  
@RestController  
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")  
public class UserController {  
    @Resource  
    private JwtUtil jwtUtil;  
  
    private final UserService userService;  
  
    public UserController(UserService userService) {  
        this.userService = userService;  
    }  
  
    @PostMapping("/login")  
    public ResultDTO login(@RequestBody @Validated UserLoginDTO userLoginDTO, HttpServletResponse response) {  
        String username = userLoginDTO.getUsername();  
        String password = userLoginDTO.getPassword();  
        User user = userService.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username));  
        if (user == null || !user.getPassword().equals(password)) {  
            return ResultDTO.error("Wrong username or password");  
        }  
        List<String> url_prefix = new ArrayList<>();  
        userService.getPermissionByUser(user).forEach(permission -> url_prefix.add(permission.getUrl()));  
        String token = jwtUtil.generateToken(username, url_prefix);  
        response.setHeader(JwtUtil.HEADER, token);  
        response.setHeader("Access-control-Expost-Header", JwtUtil.HEADER);  
        Map<String, String> map = new HashMap<>();  
        map.put("token", token);  
        user.setLastLogin(LocalDateTime.now());  
        userService.updateById(user);  
        return ResultDTO.success(map);  
    }  
  
    @GetMapping("/logout")  
    public ResultDTO logout(HttpServletRequest request, HttpServletResponse response) {  
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  
        if (authentication != null) {  
            new SecurityContextLogoutHandler().logout(request, response, authentication);  
        }  
        return ResultDTO.success();  
    }  
}