SpringBoot2.0结合JWT+Shiro实现登录以及权限的控制

在掌握了shiro以及jwt相关基础知识的前提下 

一 实现的功能:

     1: jwt结合shrio登录(登录成功后返回对应的token给前端)。

     2:shiro对接口进行授权控制。

     3:在没有登录的情况下,不允许访问未开放的接口。

     4:通过注解的方式对接口进行授权,可以设置相关角色,相关权限才可以操作

特殊说明:CpaToken是前端需要将登陆后返回的token值每次请求都携带在heard头中,用户查询等相关的sql和实体类,本篇博客不包含。另外相关的角色表,和权限表的结构每个公司的结构可能不一样,但是思想是一样的:用户表,角色表,用户角色关系表,权限表,角色权限关系表。

二 导入shiro以及jwt相关的包

 
            io.jsonwebtoken
            jjwt
            0.9.0
  
 
            org.apache.shiro
            shiro-spring
            1.4.0
 

   
            com.auth0
            java-jwt
            3.4.0
   

三 代码片段:

SpringBoot2.0结合JWT+Shiro实现登录以及权限的控制_第1张图片

1:自定义的Realm

package com.xtzn.cpa.shiro;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xtzn.cpa.entity.CpaUser;
import com.xtzn.cpa.service.impl.CpaUserServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @author:PSY
 * @date:2020/4/15
 * @description:
 */
@Slf4j
public class CpaUserRealm extends AuthorizingRealm {

    @Autowired
    private CpaUserServiceImpl userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    //    执行授权逻辑
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String username = JWTUtil.getUsername(principals.toString());
//        查询数据库中此人的权限信息,包括角色名,权限名
        CpaUser user = userService.getUser(username, null);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//        查询登录者的角色名
        simpleAuthorizationInfo.addRoles(user.getRoles());
//        查询登录者的权限信息
        simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
        return simpleAuthorizationInfo;
    }

    //执行登录认证的逻辑
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        try {
//            SimpleAuthenticationInfo中的参数返回
            String token = (String) authenticationToken.getCredentials();
            String username = JWTUtil.getUsername(token);
            if (username == null) {
                throw new AuthenticationException("token无效!");
            }
//          根据用户名查询数据库中的用户相关信息,方便对比token
            CpaUser user = userService.getOne(new QueryWrapper().lambda().eq(CpaUser::getUsername, username).eq(CpaUser::getStatus, 1));
//           验证token
            if (!JWTUtil.verify(token, user.getId(), user.getPassword())) {
                throw new AuthenticationException("账户密码错误!");
            }
            return new SimpleAuthenticationInfo(token, token, "cpaUserRealm");
        } catch (Exception e) {
            throw new AuthenticationException("token无效!");
        }

    }
}

 2:实现自定义的jwt过滤器

package com.xtzn.cpa.shiro;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author:PSY
 * @date:2020/4/16
 * @description:
 */
@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        return executeLogin(request, response);
    }

    /**
     * 执行登录的操作
     * 检测header里面是否包含CpaToken字段即可
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws AuthenticationException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("CpaToken");
        JWTToken jwtToken = new JWTToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;

    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            return executeLogin(request, response);
        }
//        return false ,代表的是没有登录或者token不正确就无法获取到对应的数据,什么都看不了。
//        如果是return true 就算是token不正确也可以看到或者操作数据
        return false;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) {
        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", "*");
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return false;
            }
            return super.preHandle(request, response);
        } catch (Exception e) {
            response401(response);
        }
        return false;
    }

    /**
     * 将非法请求跳转到 后续会用到的请求地址
     */
    private void response401(ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/user/401");
        } catch (IOException e) {
            throw new RuntimeException("重定向获取出现异常!");
        }
    }
}

3:JwtToken

package com.xtzn.cpa.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author:PSY
 * @date:2020/4/16
 * @description:
 */
public class JWTToken implements AuthenticationToken {
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }

}

4:Jwt工具类相关信息

package com.xtzn.cpa.shiro;

import com.auth0.jwt.exceptions.JWTDecodeException;
import com.xtzn.cpa.util.MDUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @author:PSY
 * @date:2020/4/16
 * @description:
 */
@Slf4j
public class JWTUtil {
    //  设置为10小时失效的token
    private static final long EXPIRE_TIME = 600 * 60 * 1000;
    //     token存入密码的盐值
    private static final String passwordSalt = "wwqr1we!";

    public static boolean verify(String token, int userId, String password) {
        try {
            Claims cpaLoginToken = Jwts.parser().setSigningKey("cpaLoginToken").parseClaimsJws(token).getBody();
            String id = cpaLoginToken.getId();
            String tokenPassword = (String) cpaLoginToken.get("password" + passwordSalt);

            if (userId == Integer.parseInt(id) && !isTokenExpired(cpaLoginToken.getExpiration()) && MDUtil.passwordEncrypt(password + passwordSalt).equals(tokenPassword))
                return true;
        } catch (Exception exception) {
            log.error("token验证失败!");
            return false;
        }
        return false;
    }

    /**
     * @param token
     * @return
     * @Title: getUsername
     * @Description: 通过token获取对应的用户名
     * @Author PSY
     * @DateTime 2020年4月16日 下午4:42:39
     */
    public static String getUsername(String token) {
        try {
            Claims cpaLoginToken = Jwts.parser().setSigningKey("cpaLoginToken").parseClaimsJws(token).getBody();
            return cpaLoginToken.getSubject();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    public static String jwtSign(String username, String password, Integer userId) {
        //当前时间
        long now = System.currentTimeMillis();
        //过期时间为1分钟
        long exp = now + EXPIRE_TIME;
        JwtBuilder builder = Jwts.builder().setId(userId.toString())
                .setSubject(username)
                //用于设置签发时间
                .setIssuedAt(new Date())
//              将密码通过盐值加密,然后放入到token中
                .claim("password" + passwordSalt, MDUtil.passwordEncrypt(password + passwordSalt))
                //用于设置签名秘钥
                .signWith(SignatureAlgorithm.HS256, "cpaLoginToken")
                //设置超时的时间
                .setExpiration(new Date(exp));
        return builder.compact();
    }

    /**
     * @return
     * @author PSY
     * @date 2020/4/17 10:10
     * @接口描述: 判断token是否过期(true过期 , false没有过期), 其实这里是多余的操作,token超时后请求悔自动的失效
     * @parmes
     */
    public static boolean isTokenExpired(Date ExpirationDate) {
        return ExpirationDate.before(new Date());
    }

}

最关键的是ShrioConfig的相关配置,注册JwtFilter以及开启shiro的注解配置,这里需要注意一个过滤链的问题,应该将        filterRuleMap.put("/**", "jwt");放在最后。

package com.xtzn.cpa.config;

import com.xtzn.cpa.shiro.CpaUserRealm;
import com.xtzn.cpa.shiro.JWTFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author:PSY
 * @date:2020/4/15
 * @description:最基本的最关键的配置类
 */
@SpringBootConfiguration
public class ShiroConfig {

    //ShiroFilterFactoryBean
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器并且取名为jwt
        Map filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        /**
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map filterRuleMap = new LinkedHashMap<>();
        // 所有的请求通过我们自己的JWT filter
        // 访问401和404页面不通过我们的Filter

        filterRuleMap.put("/user/401","anon");
        filterRuleMap.put("/lua/**", "anon");
        filterRuleMap.put("/error", "anon");
        filterRuleMap.put("/user/login", "anon");
        filterRuleMap.put("/errorResponse", "anon");
        filterRuleMap.put("/swagger-ui.html/**", "anon");
        filterRuleMap.put("/swagger-ui.html", "anon");
        filterRuleMap.put("/webjars/**", "anon");
        filterRuleMap.put("/v2/**", "anon");
        filterRuleMap.put("/swagger-resources/**", "anon");
//       注意shiro过滤链的顺序
        filterRuleMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    //    创建DefaultWebSecurityManager
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("cpaUserRealm") CpaUserRealm cpaUserRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
//      关联realm
        securityManager.setRealm(cpaUserRealm);
        return securityManager;
    }

    //创建realm
    @Bean(name = "cpaUserRealm")
    public CpaUserRealm getRealm() {
        return new CpaUserRealm();
    }


    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

}

登陆的代码,目的是获取到对应的token

  @ApiOperation("用户登录的接口")
    @PostMapping(value = "/login", produces = {"application/json;charset=UTF-8"})
    public ReturnResult login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        try {
            JSONObject jsonObject = new JSONObject();
//           根据用户的姓名和加密后的密码查询数据库中符合要求的数据
            CpaUser userForHtml = iCpaUserService.getUser(username, password);
            if (userForHtml != null) {
//           登陆成功的话生成一个token返回给前端
                String token = JWTUtil.jwtSign(username, userForHtml.getPassword(), userForHtml.getId());
//              以下为公司的业务,可忽略  
                boolean admin = userForHtml.getRoles().contains("admin");
                if (admin) {
                    userForHtml.setUserType(1);
                } else {
                    userForHtml.setUserType(0);
                }

                userForHtml.setPassword(null);
//               返回用户的信息和token信息
                jsonObject.put("OfferToken", token);
                jsonObject.put("userInfo", userForHtml);

                return ReturnResult.success(ReturnMsg.SUCCESS.getCode(), ReturnMsg.SUCCESS.getMsg(), jsonObject);
            }

        } catch (Exception e) {
            e.printStackTrace();
            return ReturnResult.error(ReturnMsg.ERROR.getCode(), e.getMessage());
        }
        return ReturnResult.error(ReturnMsg.ERROR.getCode(), "登录失败!");
    }

四:对接口通过注解进行授权,只需要在接口中增加对应的注解即可,并且添加对应的角色或者权限的信息,具体的信息可以查询相关的信息。

五:跨域的解决方式,添加一个配置即可

package com.xtzn.cpa.config;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author:PSY
 * @date:2020/4/17
 * @description:
 */
@SpringBootConfiguration
public class CORSConfiguration {
    @Bean
    public WebMvcConfigurer CORSConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("*")
                        .allowedHeaders("*")
                        //设置是否允许跨域传cookie
                        .allowCredentials(true)
                        //设置缓存时间,减少重复响应
                        .maxAge(3600);
            }
        };
    }
}

测试的结果

SpringBoot2.0结合JWT+Shiro实现登录以及权限的控制_第2张图片

你可能感兴趣的:(SpringBoot2.0结合JWT+Shiro实现登录以及权限的控制)