zoukankan      html  css  js  c++  java
  • 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的目的,对于清除登录状态有用。

    如何使用

    1. 实现一个UserStorage
    2. 配置拦截器
    3. 在登录的地方存储用户即可
    4. 保护控制器
    5. 异常拦截配置

    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);
        }
    }
    
  • 相关阅读:
    input搜索框实时检索功能实现(超简单,核心原理请看思路即可)
    django blank 和autonow
    dwebsocket的坑
    vue 动态添加active+父子传值
    NO 2,人生苦短,我学python之python+selenium元素定位
    NO 1,人生苦短,我学python之python+selenium自动化环境搭建
    SPU与SKU概念
    数据库,缓存数据一致性常用解决方案总结
    利用注解 + 反射消除重复代码
    Nacos学习与实战
  • 原文地址:https://www.cnblogs.com/xuzhen97/p/15214067.html
Copyright © 2011-2022 走看看