SpringBoot + Shiro + JWT 实现认证和授权

1、JWT

JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。

1.JWT的组成

  • JWT token的格式:header.payload.signature
  • header中用于存放签名的生成算法 {"alg": "HS512"}
  • payload中用于存放用户名、token的生成时间和过期时间{"sub":"admin","created":1489079981393,"exp":1489684781}
  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
    //secret为加密算法的密钥 String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

2.JWT实现认证和授权的原理

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

2、SpringBoot + Shiro + JWT 实现认证和授权

  1. 项目结构

在这里插入图片描述
2. 开发环境及导入相关maven依赖

  • IDEA:2018.2(lombok插件)
  • SpringBoot:2.3.1.RELEASE
  • Shiro:1.4.1
  • JWT:3.2.0
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>
  1. 集成JWT
  • JwtUti
    利用 JWT 的工具类来生成token。这个工具类有生成 token 和 校验 token 以及从token中获取信息
@Slf4j
public class JwtUtil {

    private static final Long EXPIRE_TIME = 5 * 60 * 1000L;

    private static final String SECRET = "SHIRO+JWT";

    /**
     * 生成 token 时,指定 token 过期时间 EXPIRE_TIME 和签名密钥 SECRET,
     * 然后将 expireDate 和 username 写入 token 中,并使用带有密钥的 HS256 签名算法进行签名
     * @param username
     * @return
     */
    public static String createToken(String username) {
        String token = null;
        try {
            // 过期时间
            Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            // 加密算法
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            token = JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            log.error("Failed to create token. {}", e.getMessage());
        }
        return token;
    }

    /**
     * 验证token,如果验证失败,便会抛出异常
     * @param token
     * @param username
     * @return
     */
    public static boolean verify(String token, String username) {
        boolean isSuccess = false;
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 验证token
            verifier.verify(token);
            isSuccess = true;
        } catch (UnsupportedEncodingException e) {
            log.error("Token is invalid. {}", e.getMessage());
        }
        return isSuccess;
    }

    /**
     * 在 createToken()方法里,有将 username 写入 token 中。现在可从 token 里获取 username
     * @param token
     * @return
     */
    public static String getUsernameFromToken(String token) {
        try {
            DecodedJWT decode = JWT.decode(token);
            String username = decode.getClaim("username").asString();
            return username;
        } catch (JWTDecodeException e) {
            log.error("Failed to Decode jwt. {}", e.getMessage());
            return null;
        }
    }
}
  • Account
    用户账号实体类
@Data
public class Account {
    private Integer id;
    private String username;
    private String password;
    private String perms;
    private String role;
}
  • AccountServiceImpl
    AccountServiceImpl是AccountService接口的实现类。这里采用MybatisPlus
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public Account findByUsername(String username) {
        QueryWrapper wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        return accountMapper.selectOne(wrapper);
    }
}

数据库account如下
在这里插入图片描述

  • JwtFilter
/**
 * 因为要整合了 JWT ,我们需要自定义过滤器 JWTFilter。
 * JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原方法进行了重写。
 *
 * 该过滤器主要有三步:
 * 1.检验请求头是否带有 Token: ((HttpServletRequest) request).getHeader(“Token”)
 * 2.如果带有 Token ,则执行 Shiro 中的 login() 方法,该方法将导致:
 * 将 Token 提交到 Realm 中进行验证(执行自定义的Reaml中的方法);
 * 如果没有 Token,则说明当前状态为游客状态或者其他一些不需要进行认证的接口
 * 3.如果在 Token 校验的过程中出现错误,如:Token 校验失败,
 * 那么我会将该请求视为认证不通过,则重定向到 /unauthorized/**
 * @author Sakura
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 如果请求头带有token,则对token进行检查,否则直接放行
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求头是否带有token
        if (isLoginAttempt(request,response)){
            //如果存在token,则进入executeLogin()方法执行登入,并检测 token 的正确性
            try {
                executeLogin(request,response);
            } catch (Exception e) {
                log.error("Error! {}", e.getMessage());
                responseError(response, e.getMessage());
            }
        }
        // 如果不存在 token ,则可能是执行登录操作/游客访问状态,所以直接放行
        return true;
    }


    /**
     * 检测 header 中是否包含token
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        return getTokenFromRequest(request) !=null;
    }


    /**
     * 执行登录操作
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception{
        String token = getTokenFromRequest(request);
        JwtToken jwtToken = new JwtToken(token);
        // 提交给 realm 进行登录,如果错误,会抛出异常
        getSubject(request,response).login(jwtToken);
        // 如果没有抛出异常,则代表登录成功,返回true
        return true;
    }

    /**
     * 从请求中获取token
     * @param request
     * @return
     */
    private String getTokenFromRequest(ServletRequest request) {
        HttpServletRequest req = (HttpServletRequest) request;
        return req.getHeader("Token");
    }

    /**
     * 非法请求将跳转到 "/unauthorized/**"
     */
    private void responseError(ServletResponse response, String message) {
        try {
            HttpServletResponse resp = (HttpServletResponse) response;
            // 设置编码,否则中文字符在重定向时会变为空字符串
            message = URLEncoder.encode(message, "UTF-8");
            resp.sendRedirect("/unauthorized/" + message);
        } catch (UnsupportedEncodingException e) {
            log.error("Error! {}", e.getMessage());
        } catch (IOException e) {
            log.error("Error! {}", e.getMessage());
        }
    }
}
  • JwtToken
/**
 * 这里我们自定义了一个AuthenticationToken----JwtToken。
 * 因为在Reaml认证方法中,我们是对 Token进行认证的。至于 UsernamePasswordToken (Shiro 中自带),
 * 我们需要 对 username 和 password 认证时就可以用它
 * @author Sakura
 */
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;
    }
}
  • AccountRealm
@Slf4j
public class AccountRealm extends AuthorizingRealm {
    @Autowired
    private AccountService accountService;

    @Override
    public boolean supports(AuthenticationToken token){
        return token instanceof JwtToken;
    }
    /**
     * 授权
     * 从 Token 中获取 username ,然后根据 username 可获取用户信息(角色、权限等)并添加到 AuthorizationInfo 中。
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        log.info("用户授权中");
        //获取当前登录的用户信息
        String username = JwtUtil.getUsernameFromToken(principalCollection.toString());
        Account account = accountService.findByUsername(username);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //设置权限
        info.addStringPermission(account.getPerms());
        info.addRole(account.getRole());
        return info;
    }

    /**
     * 认证
     * 拿到从 executeLogin() 方法中传过来的 Token,并对 Token 检验是否有效、用户是否存在以及是否封号
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("身份认证");
        //这里的token是从JwtFilter的executeLogin()方法传递过来的
        String token = (String) authenticationToken.getCredentials();
        //解密
        String username = JwtUtil.getUsernameFromToken(token);
        //从数据库汇总获取对应的用户名和面
        Account account = accountService.findByUsername(username);
        if (StringUtils.isEmpty(username) || !JwtUtil.verify(token,username)){
            log.error("token 认证失败");
            throw new AuthenticationException("token 认证失败");
        }
        if (null == account){
            log.error("账号或密码错误");
            throw new AuthenticationException("账号或密码错误");
        }
        log.info("用户{}认证成功!", account.getUsername());
        return new SimpleAuthenticationInfo(token, token, getName());
    }
}
  • ShiroConfig
    设置好我们自定义的 filter,并使所有请求通过我们的过滤器,除了我们用于处理未认证请求的 /unauthorized/**
@Configuration
public class ShiroConfig {

    @Bean
    public AccountRealm accountRealm(){
        return new AccountRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(accountRealm());

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

    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //设置自定义的拦截器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt",new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        Map<String,String> filterRuleMap = new HashedMap(16);
        //设置所有的请求经过自定义的filter
        filterRuleMap.put("/**","jwt");
        filterRuleMap.put("/unauthorized/**","anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    /** 对Shiro注解的支持*/
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

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

    }

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

}

在上述配置中,由于我们开启了对Shiro注解的支持,因此可以通过注解的方式来限制相关用户使用某些接口的方法,即给用户授权

@RequiresRoles("admin")
@GetMapping("/enter")
public JwtResultMap enter() {
    return resultMap.success().code(200).message("Admin Enter");
}

例如上述代码中的注解@RequiresRoles("admin")即说明需要有admin的角色才可以访问,而授权操作在AccountRealm中doGetAuthorizationInfo()方法中有设置相关的权限。

// 拥有 user 或 admin 角色,且拥有 vip 权限可以访问
@RequiresRoles(logical = Logical.OR, value = {"user", "admin"})
@RequiresPermissions("vip")
  • JwtException
    对异常进行统一地处理
@RestControllerAdvice
public class JwtException {
    @Autowired
    private JwtResultMap resultMap;

    /**捕获与Shiro相关的异常*/
    @ExceptionHandler(ShiroException.class)
    public JwtResultMap handle401(){
        return resultMap.fail().code(401).message("您没有权限访问!");
    }

    /**捕获其他异常*/
    @ExceptionHandler(Exception.class)
    public JwtResultMap globalException(HttpServletRequest request,Throwable e){
        return resultMap
                .fail()
                .code(getStatus(request).value())
                .message("访问出错,无法访问:"+e.getMessage());
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("java.servlet.error.status_code");
        if (null == statusCode){
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

}
  • JwtResultMap
    对接口中返回的数据进行统一地封装
@Component
public class JwtResultMap extends HashMap<String,Object> {

    public JwtResultMap success(){
        this.put("result","success");
        return this;
    }

    public JwtResultMap fail() {
        this.put("result", "fail");
        return this;
    }
    public JwtResultMap code(int code) {
        this.put("code", code);
        return this;
    }
    public JwtResultMap message(Object message) {
        this.put("message", message);
        return this;
    }
}
  1. 编写Controller层接口方法,模拟用户认证和授权

GuestController 游客接口

  • 不做权限处理,所有人可以访问
@RestController
@RequestMapping("/guest")
public class GuestController {
    
    @Autowired
    private JwtResultMap resultMap;

    @GetMapping("/enter")
    public JwtResultMap enter() {
        return resultMap.success().code(200).message("欢迎进入游客页面" );
    }
}

AdminController

  • enter() 方法:需要 admin 角色才能访问
@RestController
@RequestMapping("/admin")
public class AdminController {
    @Autowired
    private JwtResultMap resultMap;

    @RequiresRoles("admin")
    @GetMapping("/enter")
    public JwtResultMap enter(){
        return resultMap.success().code(200).message("Admin Enter");
    }
}

UserController

  • enter() 方法:需要 user 或 admin 角色,才能访问
  • getMessage() 方法:需要 user 或 admin 角色并且有 vip 权限,才能访问
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private JwtResultMap resultMap;

    @RequiresRoles(logical = Logical.OR,value = {"user","admin"})
    @GetMapping("/enter")
    public JwtResultMap enter(){
        return resultMap.success().code(200).message("欢迎进入用户页面");
    }

    @RequiresPermissions("vip")
    @RequiresRoles(logical = Logical.OR,value = {"user","admin"})
    @GetMapping("/getMessage")
    public JwtResultMap getMessage(){
        return resultMap.success().code(200).message("成功获得vip信息");
    }

}

LoginController 登录接口

  • login() 方法:任何人可以访问。认证成功后,会返回 Token。
  • unauthorized() 方法:权限不足时,访问的接口。
@Controller
public class LoginController {
    @Autowired
    private JwtResultMap resultMap;
    @Autowired
    private AccountService accountService;

    @PostMapping("/login")
    @ResponseBody
    public JwtResultMap login(String username,String password){
        Account account = accountService.findByUsername(username);
        if (null==account){
            return resultMap.fail().code(401).message("账号错误");
        }else if (!password.equals(account.getPassword())){
            return resultMap.fail().code(401).message("密码错误");
        }
        return resultMap.success().code(200).message(JwtUtil.createToken(username));
    }

    @ResponseBody
    @GetMapping("/unauthorized/{message}")
    public JwtResultMap unauthorized(@PathVariable String message){
        return resultMap.success().code(401).message(message);
    }
}
  1. 测试
  • 不带 Token,访问游客接口,直接放行
    在这里插入图片描述- 不带 Token,访问用户或管理员接口,显示没有权限
    在这里插入图片描述- 访问登录接口,登录信息如下,登录成功后会获得一个根据用户名生成的Token(在message)eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTY1OTc1NjEsInVzZXJuYW1lIjoienMifQ.jeac86Kar2R44hJcrjsjBvbSotKHYquZnmQHiWcD8WU
    在这里插入图片描述

在这里插入图片描述- 接着把刚才获取的Token值复制下来再重新访问用户接口,结果显示可以访问
在这里插入图片描述- 但如果用该token访问管理员接口,会显示权限不够,因为需要admin的角色才可访问
在这里插入图片描述


以上便是关于SpringBoot + Shiro + JWT 实现认证和授权

猜你喜欢

转载自blog.csdn.net/weixin_43517302/article/details/107808971