SpringSecurity:单点登录
写了很多次
本篇博客用到的技术栈:
框架 框架 ( )
工具类
先给出我的生成
(记得自行修改
package com.example.onlinespring.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 3; // 有效期3天
public static final String JWT_KEY = "myjwtkey";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
构建RBAC权限控制模型
- 用户表
- 角色表
- 权限表
- 用户-角色关联表
- 角色-权限关联表
用户实体类
package com.example.onlinespring.pojo;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
/**
* 用户实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
@NonNull
@TableField("username")
private String username;
@NonNull
@TableField("email")
private String email;
@NonNull
@TableField("password")
private String password;
@NonNull
@TableField("gender")
private Integer gender;
@NonNull
@TableField("education")
private Integer education;
@NonNull
@TableField("age")
private Integer age;
@NonNull
@TableField("isNormal")
private Integer isNormal;
@NonNull
@TableField("major")
private String major;
@NonNull
@TableField("avatar")
private String avatar;
// 标记此属性为version列对应的属性
@Version
@TableField("version")
private Integer version;
}
用户映射类
package com.example.onlinespring.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.onlinespring.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<String> selectMenuByUserId(Integer userId);
void insertRoleByUserId(Integer userId, Integer roleId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.onlinespring.mapper.UserMapper">
<insert id="insertRoleByUserId">
INSERT INTO sys_u_r
(userId, roleId)
VALUES (#{userId}, #{roleId})
</insert>
<select id="selectMenuByUserId" resultType="java.lang.String">
SELECT m.menuKey
FROM user u
LEFT JOIN sys_u_r ur ON u.id = ur.userId
LEFT JOIN role r ON r.id = ur.roleId
LEFT JOIN sys_r_m rm ON r.id = rm.roleId
LEFT JOIN menu m ON m.id = rm.menuId
WHERE u.id = #{userId}
</select>
</mapper>
这是用来封装用户信息与权限信息的实体类对象。
可以使用不同的字段来作为
package com.example.onlinespring.pojo;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* LoginUser类
* 实现UserDetails接口
*
* @author WA_automat
* @Since 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
// 私有成员User
private User user;
// 封装权限的字符串列表permissions
private List<String> permissions;
// 权限信息
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
/**
* 获取权限集合
*
* @return 权限集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
// 将permissions中的字符串封装成权限信息
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
/**
* 获取用户密码
* 调用User类的获取用户密码函数即可
*
* @return 用户密码
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* 获取用户名
* 调用User类的获取用户名函数即可
*
* @return 用户名
*/
@Override
public String getUsername() {
return user.getEmail();
}
// 下面是各种判断,改为返回true即可
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
用于在
package com.example.onlinespring.service.Impl.user;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.onlinespring.mapper.UserMapper;
import com.example.onlinespring.pojo.LoginUser;
import com.example.onlinespring.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
// 使用UserMapper查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getEmail, email);
User user = userMapper.selectOne(queryWrapper);
// 判断获取的user是否为空
// 如果为空则抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("邮箱或密码错误");
}
// 查询用户的权限信息
// List<String> list = new ArrayList<>(Arrays.asList("test", "admin"));
List<String> list = userMapper.selectMenuByUserId(user.getId());
// 把数据封装成UserDetails对象返回
return new LoginUser(user, list);
}
}
登录与登出接口
登录
@Override
@Async("asyncThreadPoolTaskExecutor")
public Future<ResponseResult> login(String email, String password) {
// AuthenticationManager 进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(email, password);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证没通过,给出对应提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
// 如果认证通过,使用userid生成有一个jwt,jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
User user = loginUser.getUser();
String userid = user.getId().toString();
String jwt = JwtUtil.createJWT(userid);
// 将token响应给前端
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
// 更换为Jedis
// 用户信息存入redis
Jedis jedis = new Jedis(host, Math.toIntExact((port)));
jedis.connect();
jedis.set("login:" + userid, JSON.toJSONString(loginUser));
// 登录有效期最长为7天(即7天后必须重新登录)
jedis.expire("login:" + userid, 7 * 24 * 60 * 60L);
// redisCache.setCacheObject("login:" + userid, loginUser);
// 获取用户名
map.put("username", user.getUsername());
return AsyncResult.forValue(new ResponseResult<>(200, "登录成功", map));
}
登出
@Override
public ResponseResult logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Integer userid = loginUser.getUser().getId();
// 删除redis中的值
Jedis jedis = new Jedis(host, Math.toIntExact((port)));
jedis.connect();
jedis.del("login:" + userid);
return new ResponseResult(200, "注销成功", null);
}
过滤器
用于判断前端传过来的
package com.example.onlinespring.filter;
import com.alibaba.fastjson2.JSON;
import com.example.onlinespring.pojo.LoginUser;
import com.example.onlinespring.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import redis.clients.jedis.Jedis;
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.Objects;
/**
* 一个Jwt认证过滤器
*
* @author WA_automat
* @since 1.0
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Long port;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取Token
String token = request.getHeader("token");
// 获取不到Token时
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
// 解析Token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token 非法");
}
// 根据Token获取用户信息
String redisKey = "login:" + userid;
// 更换为Jedis
Jedis jedis = new Jedis(host, Math.toIntExact((port)));
jedis.connect();
LoginUser loginUser = JSON.parseObject(jedis.get(redisKey), LoginUser.class);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
loginUser,
null,
loginUser.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
自定义错误类
主要是认证和授权失败的两个
package com.example.onlinespring.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* web工具类
* 暂时作为放行静态资源的工具
* 后续可能继续添加功能
*/
public class WebUtil {
public static void renderString(HttpServletResponse response, String string) {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
try {
response.getWriter().println(string);
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.onlinespring.handler;
import com.alibaba.fastjson2.JSON;
import com.example.onlinespring.utils.ResponseResult;
import com.example.onlinespring.utils.WebUtil;
import org.springframework.http.HttpStatus;
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.util.HashMap;
import java.util.Map;
/**
* 认证异常实现类
*/
@Component
public class AuthenticationEntryPointImpl
implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException, ServletException {
// 处理异常
Map<String, String> map = new HashMap<>();
map.put("state", "error");
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败,请重新登录", map);
String json = JSON.toJSONString(result);
WebUtil.renderString(response, json);
}
}
package com.example.onlinespring.handler;
import com.alibaba.fastjson2.JSON;
import com.example.onlinespring.utils.ResponseResult;
import com.example.onlinespring.utils.WebUtil;
import org.springframework.http.HttpStatus;
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.util.HashMap;
import java.util.Map;
/**
* 权限异常实现类
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException, ServletException {
// 处理异常
Map<String, String> map = new HashMap<>();
map.put("state", "error");
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足", map);
String json = JSON.toJSONString(result);
WebUtil.renderString(response, json);
}
}
最重要的 配置类
用于生成密码加密器和认证授权管理器,还有放行静态资源与公有接口的类。
package com.example.onlinespring.config;
import com.example.onlinespring.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
/**
* 创建密码加密工具
*
* @return BCryptPasswordEncoder
* @author WA_automat
* @since 1.0
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 暴露AuthenticationManager
*
* @author WA_automat
* @since 1.0
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration
) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* HttpSecurity的配置
*
* @param httpSecurity 参数
* @return SecurityFilterChain 过滤链
* @throws Exception 异常
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// httpSecurity配置
httpSecurity
.csrf().disable()
.authorizeRequests()
// OPTIONS放行
.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//下边的路径放行
.antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources", "/swagger-resources/configuration/security",
"/swagger-ui.html", "/webjars/**", "/static/images/**").permitAll()
// 放行接口
.antMatchers(
"/user/register",
"/user/login",
"/user/reset/password",
"/checkcode/register/code",
"/checkcode/reset/code"
).anonymous()
.anyRequest().authenticated();
// 配置认证过滤器
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
httpSecurity.exceptionHandling()
// 配置认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
// 配置授权失败处理器
.accessDeniedHandler(accessDeniedHandler);
// 返回过滤链
return httpSecurity.build();
}
}
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。