Springboot+Shiro+JWT+Vue后台管理系统

807 阅读3分钟

项目开发中,后台系统尤为重要,最近就想搞一套能拿来直接使用的后台管理系统。

框架:

Shiro,Shiro作为安全框架,实现登录、登出、身份验证、授权、会话管理。

JWT,Json Web Token负责访问接口时的验证。

Vue,前端框架,因为本身不是前端开发,所以Clone了某位高人在github上的模版。写的非常好,简单易用。github.com/PanJiaChen/…

前端代码就是照葫芦画瓢,不过很多地方也google,还是很麻烦的,这里不多赘述。下面整理一下后端Shiro+JWT部分代码逻辑。

Shiro

@Configuration
public class ShiroConfig {
 /*-------------------------------------------
    |             哈              哈             |
    ============================================*/
    /**
     * 验证码验证filter
     *
     * @return
     */
    @Bean(name = "captchaValidate")
    public CaptchaValidateFilter captchaValidate() {
        return new CaptchaValidateFilter();
    }
    /**
     * jwt-filter
     *
     * @return
     */
    @Bean(name = "jwt")
    public JwtFilter jwtFilter() {
        return new JwtFilter();
    }

    /**
     * 登录时账户密码验证
     *
     * @return
     */
    @Bean
    public GeneralCredentialsMatcher generalCredentialsMatcher() {
        return new GeneralCredentialsMatcher();
    }

    /**
     * 账户验证,权限验证
     *
     * @return
     */
    @Bean
    public UserRealm myRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(generalCredentialsMatcher());
        return userRealm;
    }


    @Bean(name = "shiroFilterChainDefinition")
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
        definition.addPathDefinition("/api/user/login", "captchaValidate");
        definition.addPathDefinition("/api/**", "jwt");
        return definition;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm());
        manager.setSessionManager(defaultWebSessionManager());
        manager.setCacheManager(redisCacheManager());
        return manager;
    }
    @Bean
    public RedisManager redisManager() {
        return new RedisManager();
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        return defaultWebSessionManager;
    }
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
 }
  • captchaValidate()自定义filter负责处理验证码,在login接口的时候会触发。
  • jwtFilter()继承了shiro的BasicHttpAuthenticationFilter类,重写了isAccessAllowed与onAccessDenied方法。这里这么做是解决跨域的时候,前端会发送OPTIONS探测方法,确认服务端允许跨域。这里简单处理一下让这种请求通过不被拦截,并验证如果不是OPTIONS请求,时候包含jwt的header信息
  • generalCredentialsMatcher()负责处理login验证,查询数据库验证用户名密码。
  • myRealm()自定义Realm继承shiro抽象类AuthorizingRealm,重写doGetAuthenticationInfo方法创建并保存用户信息,重写doGetAuthorizationInfo方法查询并保存用户角色权限信息。
  • shiroFilterChainDefinition()配置shiro的filter调用链,在调用login接口时会触发验证码验证的filter,而其他所有接口都会触发jwt的filter。
  • securityManager()配置DefaultWebSecurityManager,redis来保存session会话信息。
  • Redis配置,开启shiro注解配置。

登录

@PostMapping(value = "/user/login")
    public ResponseEntity<ApiResponse<LoginResponse>> login(@RequestBody @Validated LoginRequest loginRequest) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginRequest.getUsername(), loginRequest.getPassword());
        token.setRememberMe(true);
        subject.login(token);
        LoginUser user = subject.getPrincipals().oneByType(LoginUser.class);
        String sessionId = subject.getSession().getId().toString();
        String jwtToken = JwtTokenUtils.createToken(TokenParam.builder()
                .key(JwtTokenUtils.SESSION_KEY)
                .value(sessionId)
                .build());
        LoginResponse loginResponse = LoginResponse.builder()
                .name(user.getUserName())
                .token(jwtToken)
                .build();
        log.info("login success");
        return super.getApiResponseResponseEntity(loginResponse);
    }

login接口没做拦截,首先拿到当前线程的Subject实例,用接口参数封装UsernamePasswordToken对象,调用subject.login(token);调用成功之后获取登录的成功后的sessionId作为jwt的一部分,再把数据封装后返回到前端。此时session保存在redis中。

关于会有SecurityUtils.getSubject().getPrincipal()为null的问题。是因为SecurityUtils.getSubject获取的是当前线程保存在ThreadLocal中的Subject对象。而如果是前后端分离的项目的话,每次请求都不会是之前的Thread了,那么就会出现null的情况。

如果是在其他方法上配置了注解验证的话,类似@RequiresRoles("admin"),那么注解的验证会更早于其他的shiro-filter配置,注解验证时还会调用SecurityUtils.getSubject()也会出现同样的null的问题。

为了解决SecurityUtils.getSubject().getPrincipal()为null,又添加了Spring的Interceptor,这样做是为了能在注解方法之前通过token中的sessionid去redis中查找shiro登录时配置的缓存。

public class ShiroSubjectInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        String sessionId = JwtTokenUtils.getPayloadMapValue(authorization, JwtTokenUtils.SESSION_KEY);
        if(StringUtils.isBlank(sessionId)) {
            ApiResponse apiResponse = new ApiResponse();
            apiResponse.setSubCode(ApiResponseStatusCodeEnum.TOKEN_EXPIRED.getSubCode());
            apiResponse.setSubMessage(ApiResponseStatusCodeEnum.TOKEN_EXPIRED.getMessage());
            response.getWriter().write(GsonUtils.getGsonWithOutConfig().toJson(apiResponse));
            response.flushBuffer();
            return false;
        }
        Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();
        ThreadContext.bind(subject);
        return true;
    }
}

Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();从Redis取出缓存。 ThreadContext.bind(subject);绑定到当前线程的ThreadLocal中。 这样就解决了上面的问题。

vue

前端的话,大概几个功能,左侧菜单栏根据后端权限来动态加载,一些基本的权限控制,角色控制,一些增删改查。后续还会完善登录日志,操作日志。

页面

github:github.com/libinjimubo…