【Spring boot】Shiro + JWT 搭建无状态 RESTful 架构

技术栈:Spring boot + Shiro +JWT 

先说一下 Spring security 和 Shiro ,从这两者选择的时候最后还是选择了Shiro,原因是Spring security 偏重,适合大型企业项目,而且现在用Shiro的也不少。网上这两个的对比文章还是很多的,这里就不赘述。

Shiro默认实现的是session形式,也就是有状态的。我们要改变一些东西,来实现无状态的RESTful 架构~

1.  pom.xml

只需要加入这两个包就可以

<!--  shiro  -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.4.0</version>
</dependency>
<!--  JWT  -->
<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>3.8.0</version>
</dependency>

2. JWT配置

1. JWTToken.java 实体类

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;
    }
}

 2. JWTUtil.java 来实现JWT的token解析等工具类

public class JWTUtil {

    // 过期时间一小时
    private static final long EXPIRE_TIME = 60*60*1000;
    private static final Logger PLOG = LoggerFactory.getLogger(JWTUtil.class);
    /**
     * 校验token是否正确
     * @param token TOKEN
     * @param secret 用户的密码
     * @return boolean 
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            PLOG.error("JWT >> " + e);
            return false;
        }
    }

    /**
     * 无需解密直接获得token中的用户名
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            PLOG.error("JWT >> " + e);
            return null;
        }
    }

    /**
     * 生成签名,并设置过期时间
     * @param username 用户名
     * @param secret 用户的密码
     * @return token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            PLOG.error("JWT >> " + e);
            return null;
        }
    }
}

3. JWTFilter.java 我们要加一个我们自己的Shiro过滤器,并配置在Shiro

public class JWTFilter extends BasicHttpAuthenticationFilter {
    private static final Logger PLOG = LoggerFactory.getLogger(JWTFilter.class);
    /**
     * 判断是否带TOKEN请求
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return !StringUtils.isEmpty(authorization);
    }
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }
    /**
     * 这里控制通过与否
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // OPTIONS 预请求 忽略
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        // 如果不带TOKEN请求,直接阻止
        if (!isLoginAttempt(request, response)) {
            throw new AuthenticationException("token is empty");
        }
        try {
            executeLogin(request, response);
        } catch (Exception e) {
            PLOG.error("JWT >> " + e);
            responseError(request, response);
            return false;
        }

        return true;
    }
    /**
     * 将非法请求跳转到 /ign/error
     */
    private void responseError(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/user/ign/error");
        } catch (IOException e) {
            PLOG.error("JWT >> " + e);
        }
    }
}

3. Shiro配置

CustomRealm.java 

doGetAuthenticationInfo中抛出异常来进行身份判定

public class CustomRealm extends AuthorizingRealm {
    private UserMapper userMapper;

    private static final Logger PLOG = LoggerFactory.getLogger(CustomRealm.class);

    @Autowired
    private void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

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

    /**
     * 获取身份验证信息
     * Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
     *
     * @param authenticationToken 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        PLOG.info("Shiro >> 身份认证");
        String token = (String) authenticationToken.getCredentials();
        if (token == null) {
            throw new AuthenticationException("token invalid");
        }
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        // 从数据库获取对应用户名密码的用户
        String password = userMapper.getPasswordByUsername(username);
        if (null == password) {
           throw new AuthenticationException("User didn't existed!");
        }
        if (!JWTUtil.verify(token, username, password)) {
            throw new AuthenticationException("Username or password error");
        }
        return new SimpleAuthenticationInfo(token, token, getName());
    }

    /**
     * 获取授权信息
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        PLOG.info("Shiro >> 权限认证");
        String username = JWTUtil.getUsername(principalCollection.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获得该用户角色
        Integer power = userMapper.getPowerByUsername(username);
        Set<String> set = new HashSet<>();
        if (power == 100) {
            set.add("admin");
        }
        set.add("user");
        info.setRoles(set);
        return info;
    }
}
 

ShiroConfig.java

从这里加上我们上文写过的JWTFilter 以及配置一下权限控制

@Configuration
public class ShiroConfig {
    private static final Logger PLOG = LoggerFactory.getLogger(ShiroConfig.class);
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        shiroFilterFactoryBean.setLoginUrl("/user/ign/notLogin");
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/ign/notRole");

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 开放登录、未登录等映射
        filterChainDefinitionMap.put("/user/ign/**", "anon");

        // 拦截接口
        filterChainDefinitionMap.put("/user/**", "jwt");

        // 其余接口一律拦截
        // filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        PLOG.info("Shiro >> Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置 realm.
        securityManager.setRealm(customRealm());

        // 关闭 shiro 自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 自定义身份认证 realm;
     * <p>
     * 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
     * 否则会影响 CustomRealm类 中其他类的依赖注入
     */
    @Bean
    public CustomRealm customRealm() {
        return new CustomRealm();
    }

    /**
     * 下面的代码是添加注解支持
     */
    @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;
    }
}

4. 登录

登录的Controller 中,生成token并返给前端使用

    @PostMapping(value = "/ign/login")
    public ServerResponse<String> login( @RequestBody User user) {
        User sourceUser = userService.getUserByUsername(user.getUsername());
        String md5Pass = Analysis.knoveMD5(user.getPassword());
        if (sourceUser.getPassword().equals(md5Pass)) {
            PLOG.info("UserController >> login · 获取Token");
            return ServerResponse.createBySuccess(JWTUtil.sign(user.getUsername(), md5Pass));
        }
        return  ServerResponse.createByError("登录失败!");
    }

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值