数据库
数据库结构
共建五个表,分别用于存储权限信息、角色信息、权限角色对应关系、用户信息、用户角色对应关系。
以下为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执行
实体
以下实体省略
setter
、getter
、全参构造器
,实际开发请自己加上
数据库对应实体
创建
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();
}
}