springboot自定义简单的登录认证
我们想做的就是基于session的登录认证功能,如果当前用户在session中代表已经登录,否则需要登录,方法的保护我们选择保护控制器方法,如果控制器方法中有权限认证的注解则我们进行比较用户的权限列表中的code是否有方法需要的code, 如果有则通过认证否则在用户请求这个方法的时候就是403.
权限的设计可以采用rbac其实只要最终能拿到用户的权限列表和控制器上的编码比较,用什么结构,这种方式都可以实现简单的保护效果。
文件列表:
- AuthInterceptor
- NotLoginException
- NotPermissionException
- Permission
- PermissionInfo
- UserHolder
- UserInfo
- UserStorage
源代码
AuthInterceptor.java
权限和登录状态的拦截器。
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录拦截器
* @author xuzhen
*/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (UserHolder.getUser() != null) {
if(handler instanceof HandlerMethod) {
HandlerMethod h = (HandlerMethod)handler;
Permission permission = h.getMethodAnnotation(Permission.class);
// 只要方法有@Permission注解说明需要验证权限,如果权限不存在则抛出异常
if(permission != null && !UserHolder.hasPermission(permission.value())){
throw new NotPermissionException("没有权限,code:" + permission.value());
}
}
return true;
} else {
if(log.isDebugEnabled()){
log.debug("请求url: {}。",request.getRequestURI());
}
throw new NotLoginException("请先登录系统!");
}
}
}
NotLoginException.java
功能就检测没有登录时抛出的异常。
public class NotLoginException extends RuntimeException{
public NotLoginException(String errorMsg){
super(errorMsg);
}
}
NotPermissionException.java
没有权限抛出的异常。
/**
* 没有权限异常
* @author xuzhen
*/
public class NotPermissionException extends RuntimeException{
public NotPermissionException(String errorMsg){
super(errorMsg);
}
}
Permission.java
权限认证注解
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Permission {
String value() default "";
}
PermissionInfo.java
权限抽象信息,需要用户往session存的权限对象实现这个接口
/**
* 权限info
* @author xuzhen
*/
public interface PermissionInfo {
/**
* 权限编码
* @return
*/
String getCode();
}
UserInfo.java
用户抽象信息,需要用户往session存的用户对象实现这个接口
/**
* user_info 用户信息
* 必要信息,自己的用户信息要实现这个,id进行一个转义
* @author xuzhen
*/
public interface UserInfo {
/**
* 获取用户id
* @return
*/
String getUserId();
}
UserStorage.java
用户信息存储抽象,其实我们这里说是用session, 其实完全可以使用缓存,当然目前springboot session可以配置使用redis,解决了项目多实例状态共享的问题。
/**
* 用户存储,即这个工具类依赖一个实现,抽象出来就是不管你用什么存储
* 仅需要更换实现
* @param <T>
*/
public interface UserStorage<T extends UserInfo,K extends PermissionInfo>{
/**
* 登录用户存储默认key
*/
String LOGIN_USER_KEY = "currUser";
/**
* 登录用户权限存储默认key
*/
String LOGIN_USER_PERMISSION = "currPermission";
/**
* 获取用户
* @return
*/
T getUser();
/**
* 保存用户
* @param user
*/
void saveUser(T user);
/**
* 保存用户权限
* @param permissions
*/
void savePermissions(List<K> permissions);
/**
* 获取用户权限
* @return
*/
List<K> getPermissions();
/**
* 清除用户和用户的权限
*/
void cleanUser();
/**
* 清除用户和用户的权限, 自己传入session
*/
default void cleanUser(HttpSession session){
session.removeAttribute(LOGIN_USER_KEY);
session.removeAttribute(LOGIN_USER_PERMISSION);
}
}
UserHolder.java
用户信息持有者,我们可以使用这个类方便的存储删除用户信息判断权限等,重要的方法
- getUserId
- getUser
- saveUser
- savePermissions
- cleanUser
- hasPermission
import com.easyxu.snail.common.exception.CheckException;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Objects;
/**
* UserUtil 是为了获取当前用户而设计的
* 1. 用于log日志打印
* 2. 用于代码直接调取
*
* @author xuzhen
*/
@Component
public class UserHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 在spring容器中获取用户存储实例,获取不到会报错
* @param <T>
* @return
*/
private static <T extends UserInfo, K extends PermissionInfo> UserStorage<T,K> getUserStorage(){
UserStorage<T, K> storage = context.getBean(UserStorage.class);
if(storage == null){
throw new CheckException("请实现UserStorage,并注入到spring容器中。");
}
return storage;
}
/**
* 获取用户id
* @param <T>
* @return
*/
public static <T extends UserInfo, K extends PermissionInfo> String getUserId(){
UserStorage<T, K> storage = getUserStorage();
if(storage.getUser() != null){
return storage.getUser().getUserId();
}else{
return "";
}
}
/**
* 获取用户
* @param <T>
* @return
*/
public static <T extends UserInfo, K extends PermissionInfo> T getUser(){
UserStorage<T, K> storage = getUserStorage();
return storage.getUser();
}
/**
* 保存用户
* @param user
* @param <T>
*/
public static <T extends UserInfo, K extends PermissionInfo> void saveUser(T user){
UserStorage<T,K> storage = getUserStorage();
storage.saveUser(user);
}
/**
* 保存用户权限
* @param permissions 用户权限列表
* @param <T>
* @param <K>
*/
public static <T extends UserInfo, K extends PermissionInfo> void savePermissions(List<K> permissions){
UserStorage<T,K> storage = getUserStorage();
storage.savePermissions(permissions);
}
/**
* 清除用户登录信息
* @param <T>
*/
public static <T extends UserInfo, K extends PermissionInfo> void cleanUser(){
UserStorage<T,K> storage = getUserStorage();
storage.cleanUser();
}
/**
* 清除用户登录信息
* @param <T>
*/
public static <T extends UserInfo, K extends PermissionInfo> void cleanUser(HttpSession session){
UserStorage<T,K> storage = getUserStorage();
storage.cleanUser(session);
}
/**
* 判断用户是否拥有权限
* @param code 权限编码
* @param <T>
* @param <K>
* @return
*/
public static <T extends UserInfo, K extends PermissionInfo> boolean hasPermission(String code){
UserStorage<T,K> storage = getUserStorage();
return storage.getPermissions().stream()
.filter(p-> Objects.equals(code,p.getCode()))
.findFirst().isPresent();
}
}
这里其实清除用户信息的时候有一个根据session清除的方法,配合session监听我们可以实现操作指定用户session的目的,对于清除登录状态有用。
如何使用
- 实现一个UserStorage
- 配置拦截器
- 在登录的地方存储用户即可
- 保护控制器
- 异常拦截配置
1. 实现一个UserStorage
UserStorageSession.java
import com.easyxu.snail.adpter.web.auth.UserStorage;
import com.easyxu.snail.service.dto.data.LoginUserDto;
import com.easyxu.snail.service.dto.data.UserPermissionDto;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class UserStorageSession implements UserStorage<LoginUserDto, UserPermissionDto> {
@Override
public LoginUserDto getUser() {
return (LoginUserDto) RequestHolder.getSession().getAttribute(LOGIN_USER_KEY);
}
@Override
public void saveUser(LoginUserDto user) {
RequestHolder.getSession().setAttribute(LOGIN_USER_KEY, user);
}
@Override
public void savePermissions(List<UserPermissionDto> permissions) {
RequestHolder.getSession().setAttribute(LOGIN_USER_PERMISSION, permissions);
}
@Override
public List<UserPermissionDto> getPermissions() {
return (List<UserPermissionDto>)RequestHolder.getSession().getAttribute(LOGIN_USER_PERMISSION);
}
@Override
public void cleanUser() {
RequestHolder.getSession().removeAttribute(LOGIN_USER_KEY);
RequestHolder.getSession().removeAttribute(LOGIN_USER_PERMISSION);
}
}
2. 配置拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/login","/admin/logout");
}
}
3. 在登录的地方存储用户即可
当然存储的用户和权限需要实现抽象
@PostMapping("/login")
@ResponseBody
public ResponseEntity<LoginUserDto> login(@RequestBody LoginCmd cmd){
LoginUserDto loginUserDto = userService.login(cmd);
UserHolder.saveUser(loginUserDto);
List<UserPermissionDto> userPermissions = userService.getUserPermissions(UserHolder.getUserId());
UserHolder.savePermissions(userPermissions);
return ResponseEntity.ok(loginUserDto);
}
看要保存的用户对象, 权限的也类似
@Data
@Builder
public class LoginUserDto implements UserInfo {
private String id;
private String headImage;
private String nickname;
private String realName;
private String jobNumber;
@Override
@JsonIgnore
public String getUserId() {
return id;
}
}
4. 保护控制器
为了好维护权限的code,这里可以看出来注解用的是一个存储变量的接口来调用的,这样好维护。
@PatchMapping("/manager/menus/modify")
@Permission(PermissionConstant.MODIFY_MENU)
public ResponseEntity<?> modifyMenu(@RequestBody MenuModifyCmd cmd){
menuService.modifyMenu(cmd, UserHolder.getUserId());
return ResponseEntity.ok().build();
}
5. 异常拦截配置
这里还有例子没有的异常捕获,不过已经可以说明情况了
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 未登录
* @param e
* @return
*/
@ExceptionHandler(NotLoginException.class)
@ResponseBody
ResponseEntity<?> handleLoginException(NotLoginException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResult.buildNotLogin(e.getMessage()));
}
/**
* 没有权限
* @param e
* @return
*/
@ExceptionHandler(NotPermissionException.class)
@ResponseBody
ResponseEntity<?> handlePermissionException(NotPermissionException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(e.getMessage());
}
/**
* 自定义检查异常
* @param e
* @return
*/
@ExceptionHandler(CheckException.class)
@ResponseBody
ResponseEntity<?> handleCheckException(CheckException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResult.buildDefault(e.getMessage()));
}
/**
* Validated 验证异常
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
ResponseEntity<?> handleConstraintViolationException(ConstraintViolationException e){
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResult.buildDefault(e.getMessage()));
}
/**
* 默认异常处理, 超出范围的应该重点关注, 可以钉钉通知等多途径来解决问题
* @param throwable
* @return
*/
@ExceptionHandler(Throwable.class)
@ResponseBody
ResponseEntity<?> defaultException(Throwable throwable){
//TODO 应该重点关注
throwable.printStackTrace();
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResult.buildDefault(throwable.getMessage()));
}
}
ErrorResult.java 错误信息类
import lombok.Data;
/**
* 错误返回
* 10001 代表统一异常,就是前端没法做判断,直接进行展示errorMsg吧
* 10002 未登录异常
* 其它错误码请根据前台响应做出合适的选择。
* @author xuzhen
*/
@Data
public class ErrorResult {
private static final String DEFAULT = "10001";
private static final String NOT_LOGIN = "10002";
private String code;
private String msg;
private static ErrorResult build(String code, String msg){
ErrorResult response = new ErrorResult();
response.setCode(code);
response.setMsg(msg);
return response;
}
/**
* 未登录异常
* @param errorMsg
* @return
*/
public static ErrorResult buildNotLogin(String errorMsg){
return build(NOT_LOGIN, errorMsg);
}
/**
* 默认异常
* @param errorMsg
* @return
*/
public static ErrorResult buildDefault(String errorMsg){
return build(DEFAULT, errorMsg);
}
}
工具类
RequestHolder.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
public class RequestHolder {
/**
* 获取request
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest() {
log.debug("getRequest -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return null;
}
return servletRequestAttributes.getRequest();
}
/**
* 获取Response
*
* @return HttpServletRequest
*/
public static HttpServletResponse getResponse() {
log.debug("getResponse -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return null;
}
return servletRequestAttributes.getResponse();
}
/**
* 获取session
*
* @return HttpSession
*/
public static HttpSession getSession() {
log.debug("getSession -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
HttpServletRequest request = null;
if (null == (request = getRequest())) {
return null;
}
return request.getSession();
}
/**
* 获取session的Attribute
*
* @param name session的key
* @return Object
*/
public static Object getSession(String name) {
log.debug("getSession -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return null;
}
return servletRequestAttributes.getAttribute(name, RequestAttributes.SCOPE_SESSION);
}
/**
* 添加session
*
* @param name
* @param value
*/
public static void setSession(String name, Object value) {
log.debug("setSession -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return;
}
servletRequestAttributes.setAttribute(name, value, RequestAttributes.SCOPE_SESSION);
}
/**
* 清除指定session
*
* @param name
* @return void
*/
public static void removeSession(String name) {
log.debug("removeSession -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return;
}
servletRequestAttributes.removeAttribute(name, RequestAttributes.SCOPE_SESSION);
}
/**
* 获取所有session key
*
* @return String[]
*/
public static String[] getSessionKeys() {
log.debug("getSessionKeys -- Thread id :{}, name : {}", Thread.currentThread().getId(), Thread.currentThread().getName());
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
if (null == servletRequestAttributes) {
return null;
}
return servletRequestAttributes.getAttributeNames(RequestAttributes.SCOPE_SESSION);
}
}