`

shiro+jwt实现认证功能

    博客分类:
  • web
web 
阅读更多
Shiro和JWT的区别
整合之前需要清楚Shiro和JWT的区别。

首先Shiro是一套安全认证框架,已经有了对token的相关封装。而JWT只是一种生成token的机制,需要我们自己编写相关的生成逻辑。

其次Shiro可以对权限进行管理,JWT在权限这部分也只能封装到token中

1、引入依赖
<!--Shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<!--JWT-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>


和shiro相关:ShiroConfig、JWTFilter、MyRealm

2、JwtUtil
Jwt的自定义工具类,主要功能如下:

生成符合Jwt机制的token字符串

可以对token字符串进行校验

获取token中的用户信息

判断token是否过期

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**

@author: lhy

jwt工具,用来生成、校验token以及提取token中的信息
*/
@Slf4j
public class JwtUtil {
//指定一个token过期时间(毫秒)
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; //7天

/**

生成token
*/
//注意这里的sercet不是密码,而是进行三件套(salt+MD5+1024Hash)处理密码后得到的凭证
//这里为什么要这么做,在controller中进行说明
public static String getJwtToken(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret); //使用密钥进行哈希
// 附带username信息的token
return JWT.create()
.withClaim(“username”, username)
.withExpiresAt(date) //过期时间
.sign(algorithm); //签名算法
}
/**

校验token是否正确
*/
public static boolean verifyToken(String token, String username, String secret) {
try {
//根据密钥生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim(“username”, username)
.build();
//效验TOKEN(其实也就是比较两个token是否相同)
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**

在token中获取到username信息
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(“username”).asString();
} catch (JWTDecodeException e) {
return null;
}
}

public static boolean isExpire(String token){
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().getTime() < System.currentTimeMillis() ;
}
}


3、JwtToken
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;
    }
}

6、MyRealm
realm可以说是shiro两大功能:认证和授权的入口。可以自定义realm,对认证的方式进行自定义处理。重心先放在认证方法上,只要调用了subject.login(token)方法,就会进入到realm的doGetAuthenticationInfo内

public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 限定这个realm只能处理JwtToken(不加的话会报错)
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权(授权部分这里就省略了,先把重心放在认证上)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取到用户名,查询用户权限
        return null;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) {
        String token = (String) auth.getCredentials();  //JwtToken中重写了这个方法了
        String username = JwtUtil.getUsername(token);   // 获得username
       
        //用户不存在(这个在登录时不会进入,只有在token校验时才有可能进入)
        if(username == null)
            throw new UnknownAccountException();

        //根据用户名,查询数据库获取到正确的用户信息
        User user = userService.getUserInfoByName(username);

        //用户不存在(这个在登录时不会进入,只有在token校验时才有可能进入)
        if(user == null)
            throw new UnknownAccountException();

        //密码错误(这里获取到password,就是3件套处理后的保存到数据库中的凭证,作为密钥)
        if (! JwtUtil.verifyToken(token, username, user.getPassword())) {
            throw new IncorrectCredentialsException();
        }
        //toke过期
        if(JwtUtil.isExpire(token)){
            throw new ExpiredCredentialsException();
        }

        return new SimpleAuthenticationInfo(user, token, getName());
    }
}

5、JWTFilter

/**
* @author: lhy
*  jwt过滤器,作为shiro的过滤器,对请求进行拦截并处理
    跨域配置不在这里配了,我在另外的配置类进行配置了,这里把重心放在验证上
*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter{

    /**
     * 进行token的验证
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        //在请求头中获取token
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization"); //前端命名Authorization
        //token不存在
        if(token == null || "".equals(token)){
            ResultTemplate<Object> res = new ResultTemplate<>();
            res.setCode(StatusCode.UNLOGIN.getCode()).setMessage("无token,无权访问,请先登录");
            out(response, res);
            return false;
        }

        //token存在,进行验证
        JwtToken jwtToken = new JwtToken(token);
        try {
            SecurityUtils.getSubject().login(jwtToken);  //通过subject,提交给myRealm进行登录验证
            return true;   
        } catch (ExpiredCredentialsException e){
            ResultTemplate<Object> res = new ResultTemplate<>();
            res.setCode(StatusCode.TOKENEXPIRED.getCode()).setMessage("token过期,请重新登录");
            out(response,res);
            e.printStackTrace();
            return false;
        } catch (ShiroException e){ 
            // 其他情况抛出的异常统一处理,由于先前是登录进去的了,所以都可以看成是token被伪造造成的
            ResultTemplate<Object> res = new ResultTemplate<>();
            res.etCode(StatusCode.FAKETOKEN.getCode()).setMessage("token被伪造,无效token");
            out(response,res);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * json形式返回结果token验证失败信息,无需转发
     */
    private void out(ServletResponse response, ResultTemplate<Object> res) throws IOException {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        ObjectMapper mapper = new ObjectMapper();
        String jsonRes = mapper.writeValueAsString(res);
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        httpServletResponse.getOutputStream().write(jsonRes.getBytes());
    }

    /**
     * 过滤器拦截请求的入口方法
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            return executeLogin(request, response);  //token验证
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

}

6、ShiroConfig
@Configuration
@Slf4j
public class ShiroConfig {

    /**
     * 由Spring管理 Shiro的生命周期
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启对 Shiro 注解的支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    //创建ShiroFilter(用于拦截所有请求,对受限资源进行Shiro的认证和授权判断)
    //Shiro提供了丰富的过滤器(anon等),不过在这里就需要加入我们自定义的JwtFilter了
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        //配置系统的受限资源以及对应的过滤器
        Map<String, String> ruleMap = new HashMap<>();
        ruleMap.put("/user/login", "anon"); //登录路径、注册路径都需要放行不进行拦截
        ruleMap.put("/user/register", "anon");
        ruleMap.put("/**", "jwt");  // /**,一般放在最下,表示对所有资源起作用,使用JwtFilter
        shiroFilterFactoryBean.setFilterChainDefinitionMap(ruleMap);
        return shiroFilterFactoryBean;
    }


    //创建安全管理器(会自动设置到SecurityUtils中设置这个安全管理器)
    //SecurityUtils可以用来获取subject对象
    @Bean("securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //给安全管理器设置realm
        securityManager.setRealm(realm);

        //关闭shiro的session(无状态的方式使用shiro)
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    //创建自定义Realm,注入到spring容器中
    @Bean
    public MyRealm getRealm(){
        MyRealm realm = new MyRealm();
        //修改凭证校验匹配器(处理加密),只有使用了UsernamePasswordToken并且有对password进行加密的才需要
//        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//        hashedCredentialsMatcher.setHashIterations(1024);
//
//        realm.setCredentialsMatcher(hashedCredentialsMatcher);
        return realm;
    }
}

7、登录controller
@PostMapping("/login")
public ResultTemplate<String> register(@RequestBody User loginUser,
                                       HttpServletRequest request){
//根据用户名获取正确用户信息
    User user = userService.getUserInfoByName(username);
    if(user == null)
        return res.setCode(StatusCode.INVALIDUSER.getCode()).setMessage("无效用户,用户不存在");

    //盐 + 输入的密码(注意不是用户的正确密码) + 1024次散列,作为token生成的密钥
    String salt = user.getSalt();
    Md5Hash md5Hash = new Md5Hash(password, salt, 1024);

    //生成token字符串
    String token = JwtUtil.getJwtToken(username, md5Hash.toHex());  

}
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics