springboot+shiro(自定义拦截器)+jwt实现前后端分离权限管理

4 篇文章 1 订阅

说明:前后端不分离的时候springboot+shiro可以实现有状态服务,前后端分离后工程就变成无状态服务,本文直接代码解决工程无状态问题。

注:

1.了解jwt的使用

2.文章的异常为自定义异常,粘贴代码的时候可以改为runtime异常!

3.文章中的重要内容已标红

一:前期准备工作

a.引入maven

<!--token验证 jwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

<!--整合权限-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>1.2.1</version>
</dependency>

b.引入实体

用户类

@Setter
@Getter
@TableName("t_user")
public class User {
    @TableId(type = IdType.UUID)
    private String id;
    private String nickName;//用户昵称
    private String userName;//用户名
    private String passWord;//mima
    private String tele;//电话
    private Date createTime;//创建时间


    @TableField(exist = false)
    private List<Role> roleList;//用户有的角色

    @TableField(exist = false)
    private List<Res> resList;//用户有的权限
}

角色实体

@Setter
@Getter
@TableName("t_role")
public class Role {
    @TableId(type = IdType.UUID)
    private String id;
    private String roleName;//角色名称
    private String roleStatus;//角色状态  0未启用   1启用
    private Date createTime;//创建时间
}

菜单实体

@Setter
@Getter
@TableName("t_res")
public class Res {
    @TableId(type = IdType.UUID)
    private String id;
    private String resName;//资源名称名称
    private String resUrl;//资源路径
    private String resCode;//资源标识
    private String isMenu;//是否是按钮 0菜单  1按钮
    private String icon;//菜单样式
    private String pid;//父级资源
    private Date createTime;//创建时间

    /**
     * 按钮需要数据
     * @return {@link }
     * @throws
     * @author 李庆伟
     * @date 2020/5/8 10:14
     */
    @TableField(exist = false)
    private String menuIcon = null;//按钮图标
    @TableField(exist = false)
    private int checked = 0;//是否勾选checkbox

    /**
     * 左侧菜单需要数据
     * @return {@link }
     * @throws
     * @author 李庆伟
     * @date 2020/5/8 10:14
     */
    @TableField(exist = false)
    private String target = "_self";//菜单样式
    @TableField(exist = false)
    private List<my.expt.model.Res> child ;//菜单下子菜单



    public String getTitle() {
        return resName;
    }
    public String getHref() {
        return resUrl;
    }


    /*
    @TableField(exist = false)
    private Map<String,String> homeInfo ;//首页菜单  默认一直有
    @TableField(exist = false)
    private Map<String,String> logoInfo ;//LAYUI MINI菜单 默认一直有
    @TableField(exist = false)
    private Map<String,String> menuInfo ;//菜单封装 默认一直有
    */


}

二:TokenUtils工具类(其实就是jwt)

@Component
public class TokenUtil {

    public static String  key = "this is a jwt project";
    //public static long ttlMillis = 5000;//设置过期时间
    public static long ttlMillis = 30*60*100000;//设置过期时间
    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法  私匙使用用户mima
     *
     * @param user      登录成功的user对象
     * @return
     */
    public static String createJWT(User user) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("user",user);
        claims.put("userName",user.getUserName());
        claims.put("passWord",user.getPassWord());
        /*claims.put("roleList",user.getRoleList() == null || user.getRoleList().size() == 0 ? new ArrayList<Role>():user.getRoleList());
        claims.put("resList",user.getResList() == null || user.getResList().size() == 0 ? new ArrayList<Res>() :user.getResList());*/
        if(user.getResList() != null || user.getResList().size() > 0 ){
            StringBuffer sb = new StringBuffer();
            for (int a = 0; a< user.getResList().size(); a++){
                if(a != user.getResList().size()-1){
                    sb.append(user.getResList().get(a).getResCode()).append(",");
                } else {
                    sb.append(user.getResList().get(a).getResCode());
                }
            }
            claims.put("resList",sb.toString());
        }
        //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。


        //生成签发人
        String subject = user.getUserName();

        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, key);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * Token的jiemi
     * @param token 加密后的token
     * @param
     * @return
     */
    public static Claims parseJWT(String token) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }


    /**
     * 校验token
     * 在这里可以使用官方的校验,我这里校验的是token中携带的mima于数据库一致的话就校验通过
     * @param token
     * @return
     */
    public static Boolean isVerify(String token) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        if ((System.currentTimeMillis()-claims.getIssuedAt().getTime())<ttlMillis) {
            return true;
        }
        throw new MyException(ResultEnum.USER_LOTIN_TIME_OUT.getExpKey(), ResultEnum.USER_LOTIN_TIME_OUT.getExpValue());
    }


}

三:自定义拦截器(取代原来shiro的拦截校验规则)

/**
 * @author 李庆伟
 * @date 2020/7/11 13:57
 */
@Slf4j
public class CustomAuthorizationFilter extends BasicHttpAuthenticationFilter {

    private static final String TOKEN = "Authentication";

    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(TOKEN);
        return authorization != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader(TOKEN);

        JwtToken token = new JwtToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        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);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

四:重写原有的

UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,passWord);

/**
 * @author 李庆伟
 * @date 2020/7/11 14:33
 */
public class JwtToken implements AuthenticationToken {
    private static final long serialVersionUID = 1282057025599826155L;

    private String token;

    private String exipreAt;

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

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

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

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

五:shiro的配置类

/**
 * @author 李庆伟
 * @date 2020/4/23 10:54
 */
@Configuration
public class ShiroConfiguration {


    //不加这个注解不生效,具体不详
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    //将自己的验证方式加入容器
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }

    //权限管理,配置主要是Realm的管理认证
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        //添加自定义过滤器
        filters.put("jwt", new CustomAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //登录
        shiroFilterFactoryBean.setLoginUrl("/user/login");

        Map<String,String> map = new LinkedHashMap<String, String>();

        map.put("/swagger-ui.html", "anon");//swagger
        map.put("/webjars/**", "anon");
        map.put("/v2/**", "anon");
        map.put("/swagger-resources/**", "anon");//swagger

        map.put("/**","jwt");//对所有用户认证

        //map.put("/**/**", "anon");

        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    //加入注解的使用,不加入这个注解不生效
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

六:Realm授权认证

/**
 * @author 李庆伟
 * @date 2020/4/23 10:55
 */
public class MyShiroRealm extends AuthorizingRealm {

    //用于用户查询
    @Autowired
    private UserService userService;

    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    //角色权限和对应权限添加
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取登录用户名
        //User user = (User) principals.getPrimaryPrincipal();
        String token = principals.toString();

        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        if(StringUtils.isEmpty(token)){ //如果用户未登录返回没有权限
            return simpleAuthorizationInfo;
        }
        Claims claims = TokenUtil.parseJWT(token);
        String userName = (String) claims.get("userName");
        String resListIsNotAdmin = (String) claims.get("resList");
        //获取角色有的资源
        String[] arr = resListIsNotAdmin != null && StringUtils.isNotEmpty(resListIsNotAdmin) ? resListIsNotAdmin.split(",") : null;

        if(arr == null || arr.length == 0){
            return simpleAuthorizationInfo;
        }
        for(String res : arr){
            simpleAuthorizationInfo.addStringPermission(res);
        }
        return simpleAuthorizationInfo;
    }

    //用户认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        //加这一步的目的是在Post请求的时候会先进认证,然后在到请求
        String token = (String) auth.getPrincipal();
        if (token == null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, token, getName());
        return simpleAuthenticationInfo;
    }
}

七:接下来是登录和操作案例

1.控制层

/**
 * 用户登录
 * [userName, passWord]
 * @return {@link Result}
 * @throws
 * @author 李庆伟
 * @date 2020/4/26 15:19
 */
@PostMapping(value = "login",name = "user/login")
@ResponseBody
public Result login(String userName, String passWord){
    String tokenId = userService.login(userName,passWord);
    return Result.success(tokenId);
}
/**
 * 添加用户
 * [userName, passWord]
 * @return {@link Result}
 * @throws
 * @author 李庆伟
 * @date 2020/4/26 15:19
 */
@RequiresPermissions("user:add")
@PostMapping("add")
@ResponseBody
public Result add(@RequestParam(value = "userName", required = true)String userName,
                  @RequestParam(value = "nickName", required = true)String nickName,
                  @RequestParam(value = "tele", required = true)String tele){
    User user = userService.add(userName,nickName,tele);
    return Result.success(user);
}

2.接口

/**
 * 登录
 * [userName]
 * @return {@link User}
 * @throws
 * @author 李庆伟
 * @date 2020/4/23 13:36
 */
String login(String userName, String passWord);
/**
 * 用户添加
 * [userName, passWord]
 * @return {@link User}
 * @throws
 * @author 李庆伟
 * @date 2020/4/26 15:10
 */
User add(String userName, String nickName, String tele);

3.接口实现类

/**
 * 用户登录
 * [userName, passWord]
 * @return {@link User}
 * @throws
 * @author 李庆伟
 * @date 2020/4/23 13:37
 */
public String login(String userName, String passWord) {
    if(StringUtils.isEmpty(userName) || StringUtils.isEmpty(passWord)){
        throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
    }
    QueryWrapper wrapper = new QueryWrapper();
    wrapper.eq("user_name",userName);
    wrapper.eq("pass_word", Md5Util.md5(passWord));
    List<User> list = userMapper.selectList(wrapper);
    if((list == null || list.size() != 1 ) && !userName.equals("admin")){
        throw new MyException(ResultEnum.USER_LOGIN_ERROR.getExpKey(), ResultEnum.USER_LOGIN_ERROR.getExpValue());
    }
    User user = new User();
    //如果是管理员有全部权限
    if(userName.equals("admin") && passWord.equals("admin")){
        user.setId("admin");
        user.setNickName("我是管理员");
        user.setUserName("admin");
        user.setPassWord(Md5Util.md5("admin"));
        user.setTele("66666666666");
        //特殊逻辑,管理员应该有所有权限,这里暂时没有写,只模拟了非管理员的情况
    } else {
        user = list.get(0);
        List<Role> roleList = new ArrayList<Role>();
        Role role = new Role();
        role.setId("1");
        role.setRoleName("我是超级管理员");
        roleList.add(role);
        user.setRoleList(roleList);

        List<Res> resList = new ArrayList<Res>();
        Res res1 =  new Res();
        res1.setId("11");
        res1.setResCode("user:show");
        resList.add(res1);
        Res res2 =  new Res();
        res2.setId("12");
        res2.setResCode("user:add");
        resList.add(res2);
        user.setResList(resList);
    }
    Subject subject = SecurityUtils.getSubject();
    String token = TokenUtil.createJWT(user);
    JwtToken jwtToken = new JwtToken(token);
    subject.login(jwtToken);
    return token;
}
/**
 * 用户添加
 * [userName, passWord]
 * @return {@link User}
 * @throws
 * @author 李庆伟
 * @date 2020/4/26 15:11
 */
public User add(String userName, String nickName, String tele) {
    //添加用户前,判断用户名是否重复
    Map<String,Object> map = new HashMap<String,Object>();
    map.put("user_name",userName);
    if(StringUtils.isNotEmpty(userName) && userName.equals("admin")){
        throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
    }
    List<User> list = userMapper.selectByMap(map);
    if(list != null && list.size() > 0){
        throw new MyException(ResultEnum.USER_ADD_REPEAT.getExpKey(), ResultEnum.USER_ADD_REPEAT.getExpValue());
    }
    User user = new User();
    user.setUserName(userName);
    user.setPassWord(Md5Util.md5("1"));
    user.setNickName(nickName);
    user.setTele(tele);
    user.setCreateTime(new Date());
    userMapper.insert(user);
    return user;
}

 

到此文章结束。。。。。。。。。

 

 

 

 

 

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值