SpringSecurity
薛定谔see猫 2022/3/24 java框架
# 1. 快速入门
新建一个
SpringBoot
项目导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
随机访问一个
controller
,用户名为user
,密码会在控制台打印
# 2. 认证
# 2.1 工具类
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
redis
配置类@Configuration public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }
redis
使用fastjson
进行序列化public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } protected JavaType getJavaType(Class<?> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } }
统一返回对象
@Data public class R { private Integer code; private String message; private Map<String, Object> data; private R(){} public static R ok() { R r = new R(); r.setMessage("请求成功"); r.setCode(200); return r; } public static R error() { R r = new R(); r.setMessage("请求失败"); r.setCode(400); return r; } public static R condition(boolean condition, Consumer<R> ok, Consumer<R> error) { R r = null; if (condition) { r = R.ok(); ok.accept(r); } else { r = R.error(); error.accept(r); } return r; } public R conditions(boolean condition, Consumer<R> ok, Consumer<R> error) { if (condition) { ok.accept(this); } else { error.accept(this); } return this; } public R data(String key, Object value) { if (this.data == null) { this.data = new HashMap<>(); } data.put(key, value); return this; } public R data(Map<String, Object> data) { this.data = data; return this; } public R code(Integer code) { this.code = code; return this; } public R message(String message) { this.message = message; return this; } }
JWTUtils
public class JWTUtils { /** * 秘钥 */ private final static String secret = "%#$SKHJFas)("; /** * 分钟 */ private final static int time = 30; public static String getToken(String id) { return JWT.create() //payload .withClaim("id", id) //指定过期时间 .withExpiresAt(new Date(System.currentTimeMillis() + time * 60 * 1000)) //signature .sign(Algorithm.HMAC384(secret)); } /** * 检验token */ public static DecodedJWT verifyToken(String token) { return JWT.require(Algorithm.HMAC384(secret)).build().verify(token); } /** * 获取用户id */ public static String getId(String token) { DecodedJWT decodedJWT = verifyToken(token); return decodedJWT.getClaim("id").asString(); } }
# 2.2 数据库
实体类
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User implements Serializable { private static final long serialVersionUID = -40356785423868312L; @TableId(type = IdType.ASSIGN_ID) private String id; private String username; private String password; private String nickname; }
数据库
DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `username` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `nickname` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
service以及mapper
public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> { } public interface UserMapper extends BaseMapper<User> { }
yaml
配置文件spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/spring_security?allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 redis: host: 101.43.178.24 port: 6379 password: PBKXD31Zo4Tz5XVO database: 0 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
LoginUser
用户封装请求的用户@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @JSONField(serialize = false) @Override public String getPassword() { return user.getPassword(); } @JSONField(serialize = false) @Override public String getUsername() { return user.getUsername(); } @JSONField(serialize = false) public String getId() { return user.getId(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
UserDetailsServiceImpl
用于从数据库中获取用户数据并认证和授权@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("username", username); User user = userService.getOne(wrapper); if (Objects.isNull(user)) { throw new UsernameNotFoundException("用户名不存在"); } return new LoginUser(user); } }
启动测试
注:密码如果是明文存储,数据库需要存储为如下格式
{noop}xxx
# 2.3 加密密码
编写一个Security
的配置类即可
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 替换默认的密码校验
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
# 2.4 自定义登录
SecurityConfig
的配置@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 替换默认的密码校验 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 这个Bean用来认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf防护 .csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 放行的登录注册接口 .antMatchers("/user/login", "/user/register").anonymous() // 拦截除上述的所有请求 .anyRequest().authenticated(); } }
编写登录接口
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("login") public R login(@RequestBody User user){ return R.ok() .data("token", userService.login(user)) .message("登录成功"); } }
service
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public String login(User user) { // 登录认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword() ); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (ObjectUtils.isEmpty(authenticate)) { throw new RuntimeException("登录失败"); } LoginUser principal = (LoginUser) authenticate.getPrincipal(); String id = principal.getId(); // 认证通过,用户信息存储redis redisTemplate.opsForValue().set("userInfo"+id, principal); // 返回token return JWTUtils.getToken(id); } }
# 2.5 自定义过滤器
自定义过滤器,解析token
中的userid
,获取用户信息,并将其存入SecurityContextHolder
,方便SpringSecurity
后续的过滤链使用
编写过滤器
@Component public class AuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("Authorization"); // 没有token 放行 if (!StringUtils.hasText(token)) { filterChain.doFilter(request, response); return; } String id = JWTUtils.getId(token); Object o = redisTemplate.opsForValue().get("userInfo" + id); if (o == null) { throw new RuntimeException("用户未登录"); } LoginUser user = (LoginUser) o; // 这里必须使用三个参数的构造函数 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( user, null, null ); // 将用户信息存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } }
修改
config
进行注册@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationTokenFilter authenticationTokenFilter; /** * 替换默认的密码校验 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 这个Bean用来认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf防护 .csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 放行的登录注册接口 .antMatchers("/user/login").anonymous() // 拦截除上述的所有请求 .anyRequest().authenticated(); // 注册过滤器 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
# 2.6 登出
在service
删除redis
中缓存的用户信息即可
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void logout() {
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
LoginUser user = (LoginUser)authentication.getPrincipal();
redisTemplate.delete("userInfo" + user.getId());
}
}
# 3. 授权
权限字符串
模块名:实体:实体操作 // edu:user:delete
# 3.1 数据库设计
权限一般使用RBAC
模型
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_perms` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`perms_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`perms_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`role_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`username` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`nickname` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`role_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
# 3.2 授权配置
在启动类上或者配置类上开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
修改用户类
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; @JSONField(serialize = false) // 不要序列化此字段 private List<GrantedAuthority> authorities; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities == null) { authorities = new ArrayList<>(); permissions.forEach(item -> { authorities.add(new SimpleGrantedAuthority(item)); }); } return authorities; } @JSONField(serialize = false) @Override public String getPassword() { return user.getPassword(); } @JSONField(serialize = false) @Override public String getUsername() { return user.getUsername(); } @JSONField(serialize = false) public String getId() { return user.getId(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
service
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("username", username); User user = userService.getOne(wrapper); if (Objects.isNull(user)) { throw new UsernameNotFoundException("用户名不存在"); } return new LoginUser(user, userMapper.selectPermsByUserId(user.getId())); } }
# 查询用户所对应的权限 SELECT DISTINCT t_perms.perms_key FROM t_user_role LEFT JOIN t_role ON t_user_role.role_id = t_role.id LEFT JOIN t_role_perms ON t_role_perms.role_id = t_role.id LEFT JOIN t_perms ON t_perms.id = t_role_perms.role_id WHERE t_user_role.user_id = 1502174521665257474 & t_perms.perms_key IS NOT NULL
使用
@RestController public class HelloController { @GetMapping("/hello") @PreAuthorize("hasAuthority('sys:user:update')") // 一个权限 public R hello() { return R.ok() .message("hello world"); } }
# 4. 异常处理
认证失败
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); String result = JSON.toJSONString(R.error().message("认证失败")); try(PrintWriter writer = response.getWriter()) { writer.println(result); } } }
授权失败
public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(200); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); String result = JSON.toJSONString(R.error().message("权限不足")); try(PrintWriter writer = response.getWriter()) { writer.println(result); } } }
配置异常处理
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationTokenFilter authenticationTokenFilter; /** * 替换默认的密码校验 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 这个Bean用来认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf防护 .csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 放行的登录注册接口 .antMatchers("/user/login", "/user/register").anonymous() // 拦截除上述的所有请求 .anyRequest().authenticated(); // 注册过滤器 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 配置异常处理 http.exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointImpl()) .accessDeniedHandler(new AccessDeniedHandlerImpl()); } }
# 5. 跨域问题
SpringBoot的跨域
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 允许跨域的路径 registry.addMapping("/**") // 允许跨域的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 允许跨域的方法 .allowedMethods("GET", "POST", "DELETE", "PUT") // 允许跨域的请求头 .allowedHeaders("*") // 跨域的允许时间 .maxAge(3600); } }
SpringSecurity的配置
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationTokenFilter authenticationTokenFilter; /** * 替换默认的密码校验 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 这个Bean用来认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf防护 .csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 放行的登录注册接口 .antMatchers("/user/login", "/user/register").anonymous() // 拦截除上述的所有请求 .anyRequest().authenticated(); // 注册过滤器 http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 配置异常处理 http.exceptionHandling() .authenticationEntryPoint(new AuthenticationEntryPointImpl()) .accessDeniedHandler(new AccessDeniedHandlerImpl()); // 允许跨域 http.cors(); } }
# 6. 校验方法
# 6.1 Security提供
@PreAuthorize("hasAuthority('sys:user:update')") // 一个权限 public R hello() { return R.ok() .message("hello world"); }
多个权限,满足一个即可
@PreAuthorize("hasAnyAuthority('sys:user:update', 'sys:user:select')") // 多个权限 public R hello() { return R.ok() .message("hello world"); }
角色判断,注意⭐️数据库中存储的角色必须有
ROLE_
前缀@PreAuthorize("hasRole('admin')") public R hello() { return R.ok() .message("hello world"); }
多角色
@PreAuthorize("hasAnyRole('admin', 'user')") public R hello() { return R.ok() .message("hello world"); }
# 6.2 自定义校验方法
编写校验方法
@Component("meinilExperssionRoot") public class MeinilExperssionRoot { public boolean hasAuthority(String authority) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser user = (LoginUser)authentication.getPrincipal(); List<String> permissions = user.getPermissions(); return permissions.contains(authority); } }
使用SPEL表达式
@PreAuthorize("@meinilExperssionRoot.hasAuthority('sys:user:update')") public R hello() { return R.ok() .message("hello world"); }