一、自定义返回类
1. 状态码枚举
@Getter
public enum ResultCodeEnum {
SUCCESS(200, "成功"),
FAIL(400, "失败"),
USERNAME_PWD_ERROR(201, "用户名或密码错误"),
PARAM_ERROR(202, "参数不正确"),
SERVICE_ERROR(203, "服务异常"),
DATA_ERROR(204, "数据异常"),
DATA_UPDATE_ERROR(205, "数据版本异常"),
LOGIN_AUTH(208, "未登陆"),
PERMISSION(209, "没有权限"),
CODE_ERROR(210, "验证码错误"),
// LOGIN_MOBLE_ERROR(211, "账号不正确"),
LOGIN_DISABLED_ERROR(212, "该用户已被禁用"),
REGISTER_MOBLE_ERROR(213, "手机号已被使用"),
LOGIN_AURH(214, "需要登录"),
LOGIN_ACL(215, "没有权限"),
URL_ENCODE_ERROR(216, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"),
TOKEN_ERROR(218, "token无效"),
// TOKEN_ERROR(219, "token无效"),
FETCH_USERINFO_ERROR(220, "获取用户信息失败"),
;
private Integer code;
private String msg;
ResultCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
2. 封装返回体类
这是封装一个返回数据,包括返回状态码、信息、返回体数据,如:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {
/**
* 返回状态码
*/
private Integer code;
/**
* 返回信息
*/
private String msg;
/**
* 返回体数据
*/
private T data;
public static <T> ResponseResult<T> build(T data) {
ResponseResult<T> responseResult = new ResponseResult<>();
if (data != null)
responseResult.setData(data);
return responseResult;
}
public static <T> ResponseResult<T> build(ResultCodeEnum resultCodeEnum, T body) {
ResponseResult<T> responseResult = new ResponseResult<>();
responseResult.setData(body);
responseResult.setCode(resultCodeEnum.getCode());
responseResult.setMsg(resultCodeEnum.getMsg());
return responseResult;
}
public static <T> ResponseResult<T> build(Integer code, String msg) {
ResponseResult<T> responseResult = build(null);
responseResult.setCode(code);
responseResult.setMsg(msg);
return responseResult;
}
public static <T> ResponseResult<T> success(T data) {
return build(ResultCodeEnum.SUCCESS, data);
}
public static <T> ResponseResult<T> success() {
return success(null);
}
public static <T> ResponseResult<T> fail(T data) {
return build(ResultCodeEnum.FAIL, data);
}
public static <T> ResponseResult<T> fail(ResultCodeEnum resultCodeEnum, T data) {
return build(resultCodeEnum, data);
}
public static <T> ResponseResult<T> fail() {
return fail(null);
}
}
3. 封装response返回
使用流技术,向response中设置返回数据,
public class HttpResponse<T> {
public static <T> void respBack(HttpServletRequest request, HttpServletResponse response, T result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
// PrintWriter writer = response.getWriter();
Writer out= new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
}
}
上面的result返回类就可以放到里面返回,调用:
ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
HttpResponse.respBack(null, response, result);
二、自定义全局异常
1. 自定义异常
@Data
public class MyException extends RuntimeException {
/**
* 异常状态码
*/
private Integer code;
/**
* 自定义异常状态码和信息
*
* @param msg
* @param code
*/
public MyException(String msg, Integer code) {
super(msg);
this.code = code;
}
/**
* 接收枚举类
*
* @param resultCodeEnum
*/
public MyException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMsg());
this.code = resultCodeEnum.getCode();
}
@Override
public String toString() {
return "MyException{" +
"code=" + code +
", message=" + this.getMessage() +
'}';
}
}
2. 自定义异常处理器
import com.qing.common.result.ResponseResult;
import com.qing.common.result.ResultCodeEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 抛出503服务异常
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult error(Exception e) {
System.out.println("Exception:" + e.getMessage());
return ResponseResult.build(ResultCodeEnum.SERVICE_ERROR, null);
}
/**
* 捕捉自定义异常
*
* @param e
* @return
*/
@ExceptionHandler(MyException.class)
@ResponseBody
public ResponseResult error(MyException e) {
System.out.println("Exception:" + e.getMessage());
return ResponseResult.build(ResultCodeEnum.FAIL.getCode(), e.getMessage());
}
}
三、JWT
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
1. 实体类
User类:
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("jwt_user")
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
@Id
@TableField(value = "id")
private Long id;
@TableField(value = "user_name")
private String userName;
@TableField(value = "password")
private String password;
@TableField(value = "role")
private String role;
}
JWTUser类:
这里需要创建一个JwtUser,用户存储用户的一些信息,也就是springsecurity认证时用到的UserDetails,所以这里实现了UserDetails
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.ToString;
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.Collections;
import java.util.Set;
@ToString
public class JwtUser implements UserDetails {
@JsonIgnore
private static final long serialVersionUID = 1L;
public JwtUser() {
}
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
* 登录时间
*/
private Long loginTime;
/**
* 登录用户
*/
private User user;
/**
* 用户登录标识
*/
private String token;
/**
* 过期时间
*/
private Long expireTime;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 角色列表
*/
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(User user) {
this.userId = user.getId();
this.userName = user.getUserName();
this.password = user.getPassword();
this.user = user;
permissions = null;
// 这里只存储了一个角色的名字
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
public User getUser() {
return user;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Long getLoginTime() {
return loginTime;
}
public void setLoginTime(Long loginTime) {
this.loginTime = loginTime;
}
public Long getExpireTime() {
return expireTime;
}
public void setExpireTime(Long expireTime) {
this.expireTime = expireTime;
}
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public String getPassword() {
return this.password;
}
@JsonIgnore
@Override
public String getUsername() {
return this.userName;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
2. Service层
UserDetailsServiceImpl:
这里实现UserDetailsService接口,重写了loadUserByUsername
方法,因为springsecurity会自动帮我们验证所登录的用户、密码的正确性,也就是通过这个方法去查的数据库
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.findUserByUserName(s);
return new JwtUser(user);
}
}
因为是自己实现的方法,要调用userMapper:
@Mapper
public interface UserMapper extends BaseMapper<User> {
User findUserByUserName(String userName);
}
UserMapper.xml:
<?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.qing.mapper.UserMapper">
<!--selectMapId-->
<select id="findUserByUserName" parameterType="String" resultType="com.qing.pojo.User">
select *
from jwt_user
where user_name = #{userName}
</select>
</mapper>
3. 过滤器
认证过滤器JwtAuthenticationFilter
该过滤器是对用户登录时输入的账号密码进行验证,
可以追踪 authenticationManager.authenticate 源码中是调用了我们上面自定义的findUserByUserName方法去查数据库
/**
* @author qingfan
* @desc 登录验证过滤器 校验用户名密码是否正确
* @date 2022/8/27 21:48
*/
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//自定义登录路径 localhost:8080/auth/login
super.setFilterProcessesUrl("/auth/login");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
User loginUser = null;
// 从输入流中获取到登录的信息
try {
loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassword(), new ArrayList<>())
);
} catch (Exception e) {
System.out.println("Exception:" + e.getMessage());
System.out.println("用户名或密码错误,loginUser:" + loginUser.toString());
ResponseResult result = ResponseResult.fail(ResultCodeEnum.USERNAME_PWD_ERROR, null);
HttpResponse.respBack(request, response, result);
}
return null;
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
// 所以就是JwtUser
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = role + authority.getAuthority();
}
//使用用户id生成token,并且后续会用这个id作为redis主键
String token = JwtUtil.createToken(jwtUser.getUserId(), role, false);
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的格式应该是 `Bearer token`
response.setHeader("token", JwtUtil.TOKEN_PREFIX + token);
}
}
鉴权过滤器JwtAuthorizationFilter
在执行一些需要认证的接口时(比如一些CRUD接口),会走到这个过滤器,会去获取request中的token,然后进行解析,判断token是否正确,登录态是否有效等
通过uid去redis查找对应的token,使用redis管理token
/**
* @author qingfan
* @desc 鉴权过滤器 校验是否有登录态
* @date 2022/8/29 10:12
*/
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
// 如果请求头中没有Authorization信息(Cookie)则直接拦截
if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
// chain.doFilter(request, response);
System.out.println("tokenHeader:" + tokenHeader);
ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
HttpResponse.respBack(null, response, result);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader, response));
} catch (MyException | SignatureException e) {
System.out.println("Exception:" + e.getMessage());
ResponseResult<Object> result = ResponseResult.fail(ResultCodeEnum.TOKEN_ERROR, null);
HttpResponse.respBack(null, response, result);
return;
}
super.doFilterInternal(request, response, chain);
}
// token验证解析
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader, HttpServletResponse response) throws IOException {
String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
//校验是否过期
if (!JwtUtil.isExpiration(token)) {
throw new MyException(ResultCodeEnum.TOKEN_ERROR);
}
//如果token未过期,那么就拿到里面的uid,查找redis,判断是否是该uid对应token
//查找redis这里还没有实现
String uid = JwtUtil.getUsername(token);
String role = JwtUtil.getUserRole(token);
if (uid != null) {
System.out.println(new UsernamePasswordAuthenticationToken(uid, null, Collections.singleton(new SimpleGrantedAuthority(role))));
//Collections.singleton(new SimpleGrantedAuthority(role))
return new UsernamePasswordAuthenticationToken(uid, null, Collections.singleton(new SimpleGrantedAuthority(role)));
}
return null;
}
}
4. springsecurity配置类
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
// 因为UserDetailsService的实现类实在太多啦,这里设置一下我们要注入的实现类
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
// 加密密码
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 需要角色为ADMIN才能删除该资源
//.antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ROLE_ADMIN")
// 其他都放行了
.anyRequest().permitAll()
.and()
//认证拦截器
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
//鉴权拦截器
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
5. JWT工具类
jwt工具类,封装创建、校验token的方法
public class JwtUtil {
//自定义token名字,我喜欢用Cookie
public static final String TOKEN_HEADER = "Cookie";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";
// 过期时间是3600秒,既是1个小时
private static final long EXPIRATION = 3600L;
// 选择了记住我之后的过期时间为7天
private static final long EXPIRATION_REMEMBER = 604800L;
// 添加角色的key
private static final String ROLE_CLAIMS = "rol";
// 创建token
public static String createToken(Long uid, String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(String.valueOf(uid))
.setIssuedAt(new Date()) //签发时间
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) //过期时间戳
.compact();
}
// 从token中获取Subject
public static String getUsername(String token) {
return getTokenBody(token).getSubject();
}
//获取token的claims中的角色
public static String getUserRole(String token) {
return getTokenBody(token).get(ROLE_CLAIMS).toString();
}
// 是否已过期
public static boolean isExpiration(String token) {
try {
//如果过期,执行getTokenBody方法时就会抛出异常
getTokenBody(token);
} catch (ExpiredJwtException e) {
System.out.println("ExpiredJwtException:" + e.getMessage());
return false;
}
return true;
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
6. 角色权限控制
在对应的controller方法上添加@PreAuthorize注解
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId") Integer id) {
return "删除了id为:" + id + "的任务";
}