springBoot+shiro+jwt+vue(element-ui)完成用户登录接口(踩坑无数)

记录一下:企业级项目完成该功能的具体过程和原理 + 踩过的那些坑

后端项目具体过程:

首先是一些配置相关的准备工作

  1. 项目用到的一些依赖包
<properties>
        <java.version>8</java.version>
        <fastjson.version>1.2.62</fastjson.version>
        <sdk.version>4.5.0</sdk.version>
        <mybatis.version>2.1.3</mybatis.version>
        <mybatis.plus.version>3.5.1</mybatis.plus.version>
        <mysql.version>8.0.25</mysql.version>
        <dynamic.version>3.3.2</dynamic.version>
        <druid.version>1.1.23</druid.version>
        <shiro.version>3.2.1</shiro.version>
        <jwt.version>0.9.1</jwt.version>
        <hutool.version>5.6.6</hutool.version>
        <generator.version>3.2.0</generator.version>
    </properties>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!--读取配置文件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--多数据源配置-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>${dynamic.version}</version>
        </dependency>
        <!--数据库相关-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SDK-->
        <dependency>
            <groupId>com.baidu.aip</groupId>
            <artifactId>java-sdk</artifactId>
            <version>${sdk.version}</version>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!--gson json格式转换-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!--shiro-整合redis-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <!--hutool工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <!--JWT生成校验工具-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>
        <!--mybatis-plus代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${generator.version}</version>
        </dependency>
        <!--实体校验-->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
  1. 配置文件相关信息(我这里配置的多数据源,与本次内容无关,可以不加)
server:
  port: 8001
  #这里配置访问项目的http根路径,即:http://localhost:8001/picture 类似于@RequestMapping("picture")
  servlet:
    context-path: /picture
mybatis:
  mapper-locations: classpath*:com/example/**/mapping/*.xml
spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  #热部署生效
  devtools:
    restart:
      enabled: true
      #设置重启的目录,添加那个目录的文件需要restart
      additional-paths: src/main/java
      additional-exclude: WEB-INF/**
  #设置单个文件最大请求10MB,最多一次请求10个文件
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
  #"关闭缓存, 即时刷新"
  freemarker:
    cache: false
  #spring.thymeleaf.cache=true  如果开启此处会导致每次输入删除都会自动刷新哪怕你没保存
  datasource:
    druid:
      #配置初始化大小/最小/最大
      initial-size: 1
      max-active: 100
      min-idle: 1
      #获取连接等待超时时间
      max-wait: 60000
      #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      #间隔多久进行一次检测,检测需要关闭的空闲连接
      time-between-eviction-runs-millis: 60000
      #一个连接在池中最小生存的时间
      min-evictable-idle-time-millis: 300000
      #Oracle需要打开注释
      #validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        #login-username: admin
        #login-password: admin
      filter:
        stat:
          log-slow-sql: true
          merge-sql: false
          slow-sql-millis: 1000
        wall:
          config:
            multi-statement-allow: true
    dynamic:
      primary: second
      strict: false  #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源
      datasource:
        first:
          url: jdbc:mysql://127.0.0.2:3306/guns?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&nullCatalogMeansCurrent=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        second:
          url: jdbc:mysql://127.0.0.1:3306/guns?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&nullCatalogMeansCurrent=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
  main:
    allow-bean-definition-overriding: true
#jwt 相关信息
markerhub:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c09eb5
    # token有效时间 单位秒
    expire: 360
    #jwt在header中的key
    header: Authorization
#shiro redis的配置
shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379

这里的配置:markerhub是我们自定义的参数,后面的jwtutils会读取这三个参数信息,当生成token信息的时候就会通过这个秘钥加密,并设置token的过去时间

  1. 后端所有代码文件所在位置
    在这里插入图片描述
  2. 首先我们需要准备一个JwtUtils,这个类是一个token的工具类(可以直接复制使用)
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
 * jwt工具类
 * @author
 */
@ConfigurationProperties(prefix = "markerhub.jwt")
@Component
public class JwtUtils {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private String secret;
    private long expire;
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("type", "JWT")
                .setSubject(userId + "")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            logger.debug("validate is token error ", e);
            return null;
        }
    }
    /**
     * token是否过期
     * @return true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
    public String getSecret() {
        return secret;
    }
    public void setSecret(String secret) {
        this.secret = secret;
    }
    public long getExpire() {
        return expire;
    }
    public void setExpire(long expire) {
        this.expire = expire;
    }
    public String getHeader() {
        return header;
    }
    public void setHeader(String header) {
        this.header = header;
    }
}

@ConfigurationProperties(prefix = “markerhub.jwt”) 该注解会在项目启动时自动读取yml配置文件中markerhub.jwt开头的配置信息,并赋值到对应的属性上面

准备工作完成,接下来开始配置shiro的登录认证

shiro的安全验证原理:
在这里插入图片描述

Security Manager是shiro的安全管理中心,可以看到每个用户都带有一个subject对象,subject就是shiro安全管理中心用来验证的对象,是平台与shiro交互的接口,我们只需要考虑将什么样的信息传入到subject对象中让shiro去验证。
虽然Security Manager可以自行去验证subject对象信息的是否正确,但是具体的验证逻辑和验证方式是需要我们自己定义的。所以我们还需要重写shiro的验证规则对象,即realm,realm对象就是一个权限管理规则,即满足什么样的要求视为验证通过

总体流程:首先请求从前端发送过来时,会被Filter拦截(这里可以在shiroConfig中可以配置不去拦截的请求),拦截请求后需要获取header中的token信息,并校验token是否存在,如果token不存在直接响应请求失败信息,如果token存在则执行登录(这里并不是真正的登录,只是去验证了token信息),执行登录会调用我们重写的realm验证规则来验证token信息是否有效,验证通过则访问接口,否则返回错误

下面是详细流程梳理:

  1. 创建自定义token对象(继承shiro的token对象AuthenticationToken)重写方法
/**
 * tocken
 */
public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String jwt){
        this.token = jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
  1. 重写shiro的realm对象 (继承自shiro标准的AuthorizingRealm)

重写三个方法:
supports:该方法是为了使realm对象支持我们自定义的token对象
doGetAuthorizationInfo: 该方法是验证用户是否拥有某种数据操作的权限,只有当触发检测用户权限时才会调用此方法,具体验证方法自定义
doGetAuthenticationInfo:用来进行身份认证,每次访问接口时会通过该方法进行验证token信息是否有效(当我们的filter校验token存在后,就会执行shrio的登录方法,登录方法就会通过realm中这个规则进行校验token)

具体代码可参考:

/**
 * 验证机制
 * @author
 */
@Component
public class AccountRealm extends AuthorizingRealm {
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    MUserService mUserService;
    /**
     * 判断是否支持JwtToken
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    /**
     * 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("============用户授权==============");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        /*获取当前的用户,已经登录后可以使用在任意的地方获取用户的信息*/
        String userId = (String) SecurityUtils.getSubject().getPrincipal();
        return null;
    }
    /**
     * 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
     * 处理登录认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    @DS("second")
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("============用户验证==============");
        JwtToken jwtToken = (JwtToken) token;
        Claims claim = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal());
        if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
            throw new ExpiredCredentialsException("token已过期,请重新登录");
        }
        String userid = claim.getSubject();
        if (userid == null){
            throw new UnknownAccountException("账户不存在");
            //throw new LockedAccountException("账户被锁定");
        }
        //通过subject的id获取用户
        MUser byId = mUserService.getById(userid);
        if(byId == null){
            throw new UnknownAccountException("账户不存在");
        }
        //将用户信息返回给shiro
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(byId, token.getCredentials(), getName());
        return info;
    }
}

这里验证登录失败后可以直接抛出异常,因为我这里写了全局异常处理,全局异常处理会将这个错误信息响应给前端,前端会处理

@RestControllerAdvice//所有RestController 类抛异常都会被这个异常类捕获
@Log4j
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)//返回错误状态码
    @ExceptionHandler(value = RuntimeException.class)//RuntimeException异常处理方法
    public Result handler(RuntimeException e){
        return Result.isfail(e.getMessage());
    }


    @ResponseStatus(HttpStatus.UNAUTHORIZED)//返回错误状态码(该异常表示没有权限)
    @ExceptionHandler(value = ShiroException.class)//Shiro的异常处理方法
    public Result handler(ShiroException e){
        return Result.isfail(e.getMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)//IllegalArgumentException异常处理方法
    public Result handler(IllegalArgumentException e){
        return Result.isfail(500,e.getMessage());
    }

    @ExceptionHandler(Exception.class)//Exception异常处理方法
    public Result handler(Exception e){
        log.error(e.getMessage());
         return Result.isfail(500,e.getMessage());
    }
}

3.自定义拦截器,重写shiro拦截器的实现,具体实现代码:

@Component
public class JwtFilter extends AuthenticatingFilter {

    /**
     * 重写shiro的生成token的方法 (利用jwt生成自定义的token) 此后shiro会通过这个token进行login
     * @return
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //将请求转换为HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //获取请求头中的jwt信息
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)) {
            return null;
        }
        return new JwtToken(jwt);
    }
    /**
     * 拦截校验token
     * @param servletRequest
     * @param servletResponse
     * @param mappedValue
     * @return
     */
    @Override
    public boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
        //Always return true if the request’s method is OPTIONS
        if (servletRequest instanceof HttpServletRequest) {
            if (((HttpServletRequest) servletRequest).getMethod().toUpperCase().equals("OPTIONS")) {
                return true;
            }
        }
        return false;
    }
    /**
     * 拒绝访问的请求会进入这个方法处理
     * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //将请求转换为HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        //获取请求头中的jwt信息
        if (StringUtils.isEmpty(jwt)) {
//            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
//            httpResponse.getWriter().print(JSONUtil.toJsonStr(Result.isfail("token已过期,请重新登录")));
            return onLoginFailure(null,new ExpiredCredentialsException("请登录后访问该资源"),servletRequest,servletResponse);
            //jwt为空后 不需要拦截 通过后接口会通过注解进行异常处理
            //return true;
        }

        //执行登录
        return executeLogin(servletRequest, servletResponse);
    }
    /**
     * 重写登录失败的方法 失败后自定义失败响应信息
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result isfail = Result.isfail(throwable.getMessage());
        String jsonfail = JSONUtil.toJsonStr(isfail);
        HttpServletResponse servletResponse = (HttpServletResponse) response;
        try {
            //解决传输中文乱码问题
            servletResponse.setHeader("Content-Type","text/plain;charset=UTF-8");
            servletResponse.getWriter().print(jsonfail);
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
        return false;
    }
    /**
     * 处理过滤器跨域问题
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT");
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request,response);
    }
}

重点记录一下拦截过程:

访问白名单不会被拦截,其他路径会被这个拦截器拦截,首先请求到达这个拦截器会调用isAccessAllowed()方法,进行校验,校验通过返回true后直接访问接口,返回false就会进入到onAccessDenied()方法,表示改请求被拦截,在这个方法中会进行token的简单校验,校验不通过返回false,并将登录失败的信息响应给前端,校验通过会执行登录操作executeLogin(并不是登录账号,只是去通过realm校验token)通过我们重写的realm进行进一步的校验,检验通过则访问接口

  1. 接下来就可以整合shiro框架了,创建一个配置类,将我们上面自定义的realm和token适配到shiro框架中,下面是代码实现
@Configuration
public class ShiroConfig {
	
    @Autowired(required = false)
    RedisSessionDAO redisSessionDAO;

    @Autowired(required = false)
    RedisCacheManager redisCacheManager;
    
//	  自动装配filter交给shiro框架管理这是一个坑,要改为new对象的形式给shiro框架管理,具体问题下面介绍
//    @Autowired
//    JwtFilter jwtFilter;

    @Bean
    public SessionManager sessionManager(){
         DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
         //因为我本地没有启动redis,所以这里注释掉,shiro就不结合使用redis管理token信息了
//        defaultWebSessionManager.setSessionDAO(redisSessionDAO);
        return defaultWebSessionManager;
    }

    /**
     *	这个是核心,重写securityManager管理中心,将我们自定义的realm校验规则传入securityManager,
     *	下面还有一些其他的重写配置也一并交给securityManager
     * @param realms
     * @param sessionManager
     * @return
     */
    @Bean
    public SessionsSecurityManager securityManager(AccountRealm realms, SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms);

        securityManager.setSessionManager(sessionManager);

        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    /**
     * 开启注解模式
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * 配置shiro生命周期
     * @return
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        LifecycleBeanPostProcessor processor = new LifecycleBeanPostProcessor();
        return processor;
    }

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
        daap.setProxyTargetClass(true);
        return daap;
    }

    /**
     * 定义过滤器
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition(){
        // 申请一个默认的过滤器链
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String,String> filterMap = new LinkedHashMap<>(0);
//        filterMap.put("/**","authc");
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

    /**
     * 过滤器工厂业务
     * @param securityManager shiro中的安全管理
     * @param shiroFilterChainDefinition
     * @return
     */
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition){
        /*shiro过滤器bean对象*/
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        // 需要添加的过滤规则
        Map<String, Filter> filters = new HashMap<>();
        //下面这行代码是一个坑,不能使用自动装配的filter,要通过new的方式,具体问题下面介绍
		//filters.put("jwt", jwtFilter);
        filters.put("jwt",  new JwtFilter());
        shiroFilter.setFilters(filters);

        Map<String,String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        /**
         * anon:无需认证
         * authc:必须认证
         * user:如果使用rememberMe可直接访问
         * perms:该资源必须得到资源权限才可以访问
         * role:该资源必须得到资源权限才可以访问
         */
        //配置不会被拦截的链接
        filterMap.put("/login","anon");
        filterMap.put("/regist","anon");
        filterMap.put("/logout","logout");
        //添加一个jwt过滤器到过滤器链中
        filterMap.put("/**","jwt");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        //需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
        shiroFilter.setLoginUrl("/login");

        return shiroFilter;
    }

说明一下这个config配置类

因为我这个shiro框架是集成了redis的,所以在配置时要配置sessionManager(),sessionManager使用redis存储,所以自动装配了redisSessionDAO和redisCacheManager,这两个都是shiro的api,
> 重要的几个配置:
一,securityManager(),这个方法是将我们重写的一些配置交给shiro的安全管理中心,例如我们自定义的realm验证规则,和重写的sessionManager,cacheManager
二,shiroFilterFactoryBean(),在写这个配置之前要配置shiroFilterChainDefinition(),其实这两个配置共同完成了一件事情,就是指定我们自定一个Filter拦截器,并且配置一些访问白名单等(即哪些请求是可以没有token,不需要登录就能访问的)

重点说明(“/**”,“jwt”)这个配置是其他所有访问路径都会通过KEY为“jwt”的验证机制进行验证,这个“jwt”的验证机制就是我们在上面添加的Filter,所以这两个KEY要一致,这里你也可以配置多个filter,可以实现不同的请求会走不同的拦截器进行拦截
其他的配置基本就是一些shiro的标准配置,声明周期、开启注解模式,这些都是什么东西可以在网上得到答案

这里记录一下上面的几个坑:

刚开始我使用的是自动装配JwtFilter拦截器,然后在配置中将这个拦截器交给securityManager安全管理中心去管理,结果导致我配置的访问白名单不生效,所有的请求都会被拦截,就很坑,所以这里必须通过new对象的形式去管理
还有一个坑就是添加的访问请求白名单一定要在jwt过滤之前put到map中,因为这个map是一个LinkedHashMap,有序map,否则白名单不生效

上面的流程基本就是shiro验证的过程
接下来就是shiro验证通过访问到接口之后的操作了:(这里我只用了登录和注册,进行测试的,实际情况中这两个请求要配置白名单,不需要验证token)

 	@DS("second")
    @DSTransactional
    @PostMapping(path = "/login")
    public Result login(@Validated @RequestBody MUser mUser) {
        QueryWrapper<MUser> wrapper = new QueryWrapper<>();
        wrapper.eq("username", mUser.getUsername());
        MUser one = mUserService.getOne(wrapper);
        if (one == null){
            return Result.isfail("用户名密码不正确");
        }
//        Assert.notNull(one, "用户名密码不正确");
        if (!one.getPassword().equals(SecureUtil.md5(mUser.getPassword()))) {// md5加密
            return Result.isfail("用户名密码不正确");
        }
        String jwt = jwtUtils.generateToken(one.getId());// 生成jwt
        // 返回用户信息
         return Result.isSucc(MapUtil.builder()
                .put("id", one.getId())
                .put("username", one.getUsername())
                .put("avatar", one.getAvatar())
                .put("email", one.getEmail())
                .put("jwt", jwt)// 生成的jwt
                .map());
    }

登录成功后会通过用户信息生成token信息(这里我把用户id存储到subject中生成jwt信息,可以根据自己需要将自己特定的信息存储,下次访问接口时realm也要验证这信息),然后将jwt信息响应到前端页面

到此后端所要做的事情基本完成了,要实现每次请求都携带这个jwt信息就要通过前端进行完成了

前端项目具体过程(从创建项目开始配置):

  1. 首先你需要创建一个空白的vue项目(具体过程就是一段命令,可以自己百度),然后我们开始一步一步进行配置
    下面是目录结构和涉及到的文件
    在这里插入图片描述

我们需要知道vue项目启动之后入口页面都是App.vue页面,这个页面可以当做其他页面的模板页面

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

这里的<router-view/>标签就是用来通过router路由跳转其他页面,例如我们访问前端地址(http://localhost:8080)时需要直接访问登录页面,但是直接访问这个地址会默认访问App.vue页面的,所以需要通过路由做一个重定向跳转一下,登录页面就会镶嵌到这个div中
只有这个标签是没有办法做到跳转的,还需要在路由router的index.js文件中配置一个redirect重定向路径

import Vue from 'vue'
import Router from 'vue-router'

import HelloWorld from '@/components/HelloWorld'
import demoFile1 from '@/business/demo1/demoFile1'
import loginPage from '@/business/login/index'

Vue.use(Router)

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push(location) {
   return originalPush.call(this, location)
  //  .catch(err => err)
}

export const routes = [
  { path: '/', redirect:'/login'},
  { path: '/login' , component: loginPage, name: 'login'},
  { path: '/demo1', name: 'demo1', component: demoFile1 },
  { path: '/HelloWorld', name: 'HelloWorld', component: HelloWorld },
]
export default new Router({
  routes
})

{ path: ‘/’, redirect:‘/login’} 这个配置就是做重定向的关键,当访问http://localhost:8080这个路径时,就会触发路由的重定向到http://localhost:8080/#/login这个路径
其他三个就是配置各自页面的路由地址

  1. 接下来就是引入element-ui、axios、VueRouter,具体就是通过main.js文件进行全局引入,这个整个项目的入口js文件
import Vue from 'vue';
import App from './App';
import VueRouter from 'vue-router';
import router from './router';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
//引入axios
Vue.prototype.$axios = axios;

Vue.use(VueRouter);
Vue.use(ElementUI,{ size: 'small', zIndex: 3000 });
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
  render: h => h(App)
})

引入之后,接下来就是整合axios,axios作为ajax请求的封装,我们要再次封装一下request等请求,因为我们需要在每次请求时拦截请求并将登录之后的token信息存入到请求头header中
所以我们需要创建两个工具类:auth.js、request.js

auth.js 是一个针对token的get,set,remove等方法集合,我这里使用sessionStorage存储token信息
request.js 就是对axios的request和response的一个封装

具体代码:
auth.js

const TokenKey = 'loginToken'

export function getToken(){
    return sessionStorage.getItem(TokenKey)
}

export function setToken(token) {
   return sessionStorage.setItem(TokenKey, token)
 }
  
export function removeToken() {
  return sessionStorage.removeItem(TokenKey)
}

request.js

import axios from "axios";
import { getToken } from "./auth";
import { Message, MessageBox } from 'element-ui'

//创建axios实例
const service = axios.create({
    baseURL: process.env.BASE_API, // api的base_url
    timeout: 60000 // 请求超时时间
});

service.interceptors.request.use(config => {
    // Do something before request is sent
    config.headers['Content-Type'] = 'application/json;charset=UTF-8';
    config.headers['Access-Control-Expose-Headers'] = 'Authorization';
    config.headers['Authorization'] = getToken()
    return config;
}, error => {
    //当请求出错时我们需要处理的逻辑可以写在这里
    console.log(error); // for debug
    //报错后抛出异常 我们需要捕获异常再处理
    Promise.reject(error);
});

service.interceptors.response.use(
    response => {
        const res = response.data;
        if (res.code !== 200) {
            Message({
                message: res.message,
                type: 'error',
                duration: 3 * 1000
            })

            // 401:未登录;
            if (res.code === 401) {
                MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
                    confirmButtonText: '重新登录',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                    store.dispatch('FedLogOut').then(() => {
                        location.reload()// 为了重新实例化vue-router对象 避免bug
                    })
                })
            }

            // return Promise.reject('error');
            return Promise.reject(res);
            // return res
        } else {
            return response.data;
        }
    },
    error => {
        console.log('err' + error.message); // for debug
        Message({
            message: error.message,
            type: 'error',
            duration: 3 * 1000
        })
        return Promise.reject(error);
    }
)

export default service;

这个文件我们需要引入axios、auth.js、element-ui(需要提示失败等信息)

需要注意的是:
创建axios实例的两个参数,baseURL(请求host路径)、timeout(请求超时时间)
重点是baseURL,这个值为process.env.BASE_API,如果你的项目启动的时候是npm run dev的话,BASE_API使用的就是dev环境配置的BASE_API
这个配置在config目录下的dev.env.js文件中,prodEnv是打包时使用的配置信息,npm run build时会将prod.env.js文件的BASE_API地址打包进去,我们本地运行时会采用下面的这个配置地址

http://localhost:8001/picture 配置后面的这个/picture地址是因为,后端项目的配置文件中配置了访问更路径

const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  BASE_API: '"http://localhost:8001/picture"',
})

host地址配置完成,接下来就是配置请求和响应的封装处理

service.interceptors.request 意思就是每次去拦截request请求
config.headers[‘Content-Type’] = ‘application/json;charset=UTF-8’;
config.headers[‘Access-Control-Expose-Headers’] = ‘Authorization’;
config.headers[‘Authorization’] = getToken()
然后向request请求的header中添加这几个参数信息,将token信息添加到请求中

service.interceptors.response 每次拦截响应信息,如果返回code 不成功则给予响应的提示信息
如果 return Promise.reject(res); 则代表抛出异常,这时你的请求代码中需要捕获异常,再异常中处理返回信息,并提示错误
如果 return res; 则代表将响应信息返回给你的请求代码,你可以自行处理这个响应信息
两者的区别就是 一个需要你捕获异常,另一个不需要捕获异常,下面代码会有演示

接下来就是在login页面 完成登录了

  1. 首先创建一个login.js 封装一下登录方法
    代码如下
import request from '@/utils/request'

export function Login(user) {
  return request({
    url: '/login',
    method: 'post',
    data: user
  })
}

然后来到登录页面,index.vue页面

import { register, Login } from '@/api/Login';
export default{
//...这里其他的组件就不写了
 methods: {
 	handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {//表单信息校验通过
          this.loading = true;
          Login(this.loginForm).then(res => {
            this.loading = false;
            if (res.code == 200) {
              this.$message({
                message: '登录成功',
                type: 'success'
              });
              sessionStorage.setItem('username', this.loginForm.username);
              sessionStorage.setItem('password', this.loginForm.password);
              sessionStorage.setItem('loginToken', res.data.jwt);
              this.$router.push({ path: '/demo1' })
            } else {
              removeToken
              this.$message.error(res.message);
            }
          }).catch(() => {
            removeToken
            this.loading = false
          })
        } else {
          console.log('参数验证不合法!');
          return false
        }
      })
    }
 }
}

登录请求成功后就会将token信息存入到sessionStorage,下次请求就会将这个token信息携带到请求头中了

先记录到这吧。。。。。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值