Project1_05_Shiro+JWT+SpringBoot+Vue ===>邮箱注册登录+短信注册登录+alipay支付

一、项目环境

  1. 前端页面:Vue
  2. Project SDK:JDK1.8

二、项目演示

  1. 项目源码:shoppingProject01_pub : version5.0.1;shoppingProject01_pub : version5.0.2
  2. 项目参考:Project04;不良人_JWT;不良人_Shiro;不良人_Swagger;狂神说_Git
  3. 项目运行注意事项:同Project04
  4. 项目运行过程:
    项目网址:shoppingProject01_dev【若无法访问是我在维护】

三、主要模块说明

1 Shiro模块说明:

1.1 Shiro的概述:
(1) Shiro和SpringSecurity是权限管理的两个主流框架。
(2) 权限管理包括 1) 用户身份认证;2) 用户授权 两部分。对需要进行访问控制的用户首先需要经过身份验证,认证通过后的用户具有指定资源的访问权限才可以访问。

1.2 本次实现中Shiro的作用:
(1) 在用户未登录之前只能访问登录注册页面,登录之后可以访问商品页面。

1.3 Shiro框架的整合:
本次实现了项目与Shiro的用户身份认证功能的整合–shoppingProject01_pub : version5.0.1,具体过程如下:
Step1:引依赖

        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-spring-boot-starterartifactId>
            <version>1.5.3version>
        dependency>

Step2:整合Shiro框架
com.salieri.config.ShiroConfig:

// 用来整合shiro框架相关的配置类
@Configuration
public class ShiroConfig {

    // 配置ShiroDialect:用于thymeleaf和shiro标签配合使用
    // 要加这个bean,shiro标签才能在thymeleaf中起效果
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

    // 1. 创建shiroFilter,负责拦截所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 配置系统受限资源
        // 配置系统公共资源
        Map<String, String> map = new HashMap<>();
        map.put("/user/*", "anon");              // anon 设置为公共资源,负责放行资源
        map.put("/css/*", "anon");
        map.put("/img/*", "anon");
        map.put("/js/*", "anon");
        map.put("/login.html", "anon");
        map.put("/registEmail.html", "anon");
        map.put("/registPhone.html", "anon");
        map.put("/registNickName.html", "anon");
        map.put("/test02.html", "anon");
        map.put("/item/alipay/notifyUrl","anon");
        map.put("/**", "authc");                 // authc 请求这个资源需要认证和授权
        // 默认认证界面路径
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    // 2. 创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 给安全管理器设置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    // 3. 创建自定义realm
    @Bean
    public Realm getRealm() {

        UserRealm userRealm = new UserRealm();

        // 修改凭证校验匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置加密算法为md5
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置散列次数
        credentialsMatcher.setHashIterations(1024);
        userRealm.setCredentialsMatcher(credentialsMatcher);

        return userRealm;
    }
}

Step3: 创建自定义Realm
con.salieri.shiro.realms.UserRealm

 // 自定义realm
public class UserRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    // 身份验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("=========");
        // 获取身份信息(由UserServiceImpl,loginAccount传入)
        String principal = (String) token.getPrincipal();
        // 在工厂中获取service对象
        UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
        List<User> userList = userService.selectUserByName(principal);
        User user = userList.get(0);
        if ( !ObjectUtils.isEmpty(user) ) {
            return new SimpleAuthenticationInfo(user.getName(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
        }
        return null;
    }
}  

Step4: 创建工厂类对象
com.salieri.utils.ApplicationContextUtils

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override  // springboot启动时会将工厂以参数形式回传给该方法
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }


    // 由此可根据bean名字获取工厂中指定的bean对象
    public static Object getBean(String beanName){
        return context.getBean(beanName);
    }

}

1.4 基于Shiro的用户认证过程的实现:
1 ) 用户注册账号时,对明文密码进行md5+salt+hash散列

    // 注册账号
    @Override
    public int createAccount(User user,String confirmCode) {

        // 判断激活的key
        this.activeKey = "ActiveCode" + user.getName() + ":isAlive";
//        // 过期时间为5分钟
//        redisTemplate.opsForValue().set(activeKey,"alive",5, TimeUnit.MINUTES);

        // 生成随机盐(用于加密)
        String salt = SaltUtils.getSalt(8);
        // 明文密码进行md5+salt+hash散列
        Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
        // 初始化账号信息
        user.setSalt(salt);
        user.setPassword(md5Hash.toHex());
        user.setConfirmCode(confirmCode);
        user.setIsValid((byte) 0);

        return userDAO.insertUser(user);
    }

2 )用户登录账号时,利用Shiro方法做如下判断:

        // 情况3:查询到一个用户时
        // 获取主体对象
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.login(new UsernamePasswordToken(user.getName(), user.getPassword()));
            map.put("state",true);
            map.put("msg","登录成功");
            return map;
        } catch ( UnknownAccountException e ) {
            e.printStackTrace();
            map.put("state",false);
            map.put("msg","用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            map.put("state",false);
            map.put("msg","密码错误");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state",false);
            map.put("msg","登录失败");
        }
        return map;
    }

此时 UsernamePasswordToken 会调用 com.salieri.config.ShiroConfig ,实现解密验证。
3 )用户退出账号时,利用如下代码段实现认证权限退出:

    public Map<String,Object> logout() {
        Map<String,Object> map = new HashMap<>();
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();  // 退出用户
            map.put("state",true);
            map.put("msg","提示:退出账户成功");
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state",false);
            map.put("msg","提示:退出账户失败");
        }
        return map;
    }

2 JWT模块说明:

2.1 JWT的概述:
2.1.1 服务器端利用session&cookie存储用户信息存在问题如下:
Project1_05_Shiro+JWT+SpringBoot+Vue ===>邮箱注册登录+短信注册登录+alipay支付_第1张图片
2.1.2 JWT认证流程:
Step1: 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
Step2:后端核对用户名和密码成功后,将用户的id等其他信息作为JTW Payload(负载),将其余头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token格式:head.payload.singurater
Step3:后端将JWT字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可
Step4:后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确:检查Token是否过期;检查Token的接收方是否是自己(可选)。
Step5:验证通过后,后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
2.1.3 JWT优势:
1)) 简洁(Compact):可以通过URL,POST参数或者在HTPP header发送,因为数据量小,传输速度也很快。
2)) 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
3)) 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4)) 不需要在服务端保存会话信息,特别适用于分布式微服务。
4 ) JWT结构:
Project1_05_Shiro+JWT+SpringBoot+Vue ===>邮箱注册登录+短信注册登录+alipay支付_第2张图片
2.1.4 加拦截器:
给接口加token验证确实能对接口起到一定的保护,但每个接口都做JWT,代码冗余度太高,故在单体应用下,可用拦截器去做。在分布式系统中,可用网关去解决。

2.2 本次实现中JWT的作用:
(1) 在用户未登录之前只能访问登录注册页面,登录之后可以访问商品页面。

2.3 JWT的整合过程:
本次实现了项目与JWT的用户身份认证功能的整合–shoppingProject01_pub : version5.0.2,具体过程如下:
Step1: 创建JWTUtils工具类

public class JWTUtils {

    private static final String SIGN = SaltUtils.getSalt(6);

    // 生成token ---->   header.payload.sign
    public static String getToken(Map<String,String> map){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7);  // 默认7天过期
        // 创建jwt builder
        JWTCreator.Builder builder = JWT.create();
        // payload
        map.forEach((k,v)->{
            builder.withClaim(k,v);
        });
        String token = builder.withExpiresAt(instance.getTime())   // 指定过期时间
                              .sign(Algorithm.HMAC256(SIGN));      // 签名
        return token;
    }

    // 验证token合法性,获取token信息
    public static DecodedJWT verify(String token) {
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

}

Step2: 创建JWT拦截器
com.salieri.interceptors.JWTInterceptor

// 拦截器,HandlerInterceptor由springmvc提供
public class JWTInterceptor implements HandlerInterceptor {

    // 预处理拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Map<String,Object> map = new HashMap<>();
        // 获取请求头中的令牌
        String token = request.getHeader("token");

        try {
            DecodedJWT verify = JWTUtils.verify(token);   // 验证令牌
            return true; // 放行请求
        } catch (SignatureVerificationException e) {
            e.printStackTrace();
            map.put("msg","无效签名");
        } catch (TokenExpiredException e) {
            e.printStackTrace();
            map.put("msg","token过期");
        } catch (AlgorithmMismatchException e) {
            e.printStackTrace();
            map.put("msg","token算法不一致");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("msg","token无效");
        }
        map.put("state",false);  // 设置状态
        // 将map转为json
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }

}

Step3: 配置授权页面
com.salieri.config.InterceptorConfig

@Configuration // 为了被springboot扫描到
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/**")                   // 其他接口token验证保护
                .excludePathPatterns("/user/**")          // 应该所有用户都放行
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/registEmail.html")
                .excludePathPatterns("/registNickName.html")
                .excludePathPatterns("/registPhone.html")
                .excludePathPatterns("/itemlist.html")
                .excludePathPatterns("/item.html")
                .excludePathPatterns("/activation-account.html")
                .excludePathPatterns("/successpay.html")
                .excludePathPatterns("/swagger-ui.html")
                .excludePathPatterns("/item/payForItem")
                .excludePathPatterns("/css/*")
                .excludePathPatterns("/img/*")
                .excludePathPatterns("/js/*");
    }
}

Step4: 前端发送用户名验证码,后端验证通过,向前端发送token:

// 情况3:查询到一个用户时
        try {
            User userDB = userListDB.get(0);
            // 用户输入的密码和盐进行加密
            String md5Pwd = SecureUtil.md5(user.getPassword() + userDB.getSalt());
            // 密码不一致,返回:用户名或密码错误
            if ( !userDB.getPassword().equals(md5Pwd) ) {
                map.put("state",false);
                map.put("msg","提示:用户名或密码错误");
                return map;
            }
            Map<String,String> payload = new HashMap<>();
            payload.put("confirmCode",userDB.getConfirmCode());
            payload.put("name",userDB.getName());
            // 生成JWT令牌
            String token = JWTUtils.getToken(payload);
            map.put("state",true);
            map.put("msg","认证成功");
            map.put("token",token);    // 响应token
        } catch (Exception e) {
            map.put("state",false);
            map.put("msg",e.getMessage());
        }
        return map;

Step5: 前端获得token,存储、及之后前端访问后端接口时携带token的方法,参考axios发送post和get请求的时候,带请求头的写法


localStorage.setItem("token",res.data.token);


var token = localStorage.getItem("token");
that = this;
 axios.get("/salieri/item/getItemById?itemid=" + this.itemid,
      {
         headers:{
                  'token':token
                 }
      }
      ).then((res) => {
             that.item = res.data;
})


var token = localStorage.getItem("token");
var that = this;
var itemid = that.itemid;
axios.post("/salieri/order/alipay/callback",
          {itemid: itemid},
              {
                 headers:{
                      'token':token
                 }
              }
 ).then(function (res) {
        console.log("res======>",res);
        if (res.data.code == 20000) {
              alert("该商品已被购买");
        } else {
              alert("该商品未被购买");
        }
})
// 清空浏览器中token的方法
localStorage.removeItem("token");

你可能感兴趣的:(JWT学习,Shiro学习,java,vue,shiro,jwt)