Springboot实战:Shiro+jwt前后端分离超详细教程(附git源码下载)

本文详细介绍了如何使用Springboot结合Shiro和JWT来构建一个无状态的身份验证系统。通过JWT进行跨域身份验证,减少了服务器资源消耗,支持CDN,并且适用于移动端。文章涵盖了配置Shiro、JWT工具封装、 Realm整合、过滤器设置以及登录注册实战。提供了完整的代码示例和效果演示。
摘要由CSDN通过智能技术生成

Springboot-cli 开发脚手架系列



简介

Springboo配置Shiro+jwt进行登录校验,权限认证,附demo演示。

  • 什么是JWT
    jwt 全称JSON Web Tokens,是目前最流行的跨域身份验证解决方案。

  • 验证流程
    在这里插入图片描述

  • 这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

    1.支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
    2.无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
    3.更适用CDN:可以通过内容分发网络请求服务端的所有资料
    4.更适用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多
    5.无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御

1. Springboot实战完整教程

  • pom.xml依赖
      <!-- Shiro核心框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.9.0</version>
        </dependency>
        <!-- Shiro使用Spring框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.9.0</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--   jwt java api支持     -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.80</version>
        </dependency>
  • application.yml配置文件中配置jwt的秘钥及有效时间
server:
  port: 9999

jwt:
  # 密钥
  secret: xxxxx.xxxx.xxxx
  # 有效期(秒)
  expire: 86400

2. 封装jwt工具

  • TokenProvider 用于生成及校验token
/**
 * token管理
 *
 * @author Ding
 */
@Slf4j
@Component
public class JwtProvider {

    @Value("${jwt.expire}")
    private Integer expire;

    @Value("${jwt.secret}")
    private String secret;

    /**
     * 生成token
     *
     * @param userId 用户id
     */
    public String createToken(Object userId) {
        return createToken(userId, "");
    }

    /**
     * 生成token
     *
     * @param userId   用户id
     * @param clientId 用于区别客户端,如移动端,网页端,此处可根据自己业务自定义
     */
    public String createToken(Object userId, String clientId) {
        Date validity = new Date((new Date()).getTime() + expire * 1000);
        return Jwts.builder()
                // 代表这个JWT的主体,即它的所有人
                .setSubject(String.valueOf(userId))
                // 代表这个JWT的签发主体
                .setIssuer("")
                // 是一个时间戳,代表这个JWT的签发时间;
                .setIssuedAt(new Date())
                // 代表这个JWT的接收对象
                .setAudience(clientId)
                // 放入用户id
                .claim("userId", userId)
                // 自定义信息
                .claim("xx", "")
                .signWith(SignatureAlgorithm.HS512, this.getSecretKey())
                .setExpiration(validity)
                .compact();
    }

    /**
     * 校验token
     */
    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(this.getSecretKey()).parseClaimsJws(authToken);
            return true;
        } catch (Exception e) {
            log.error("无效的token:" + authToken);
        }
        return false;
    }

    /**
     * 解码token
     */
    public Claims decodeToken(String token) {
        if (validateToken(token)) {
            Claims claims = Jwts.parser().setSigningKey(this.getSecretKey()).parseClaimsJws(token).getBody();
            // 客户端id
            String clientId = claims.getAudience();
            // 用户id
            Object userId = claims.get("userId");
            log.info("token有效,userId:{}", userId);
            return claims;
        }
        log.error("***token无效***");
        return null;
    }

    private String getSecretKey() {
        return Base64.getEncoder().encodeToString(secret.getBytes(StandardCharsets.UTF_8));
    }
}

3. shiro和JWT整合

  • 先介绍我们要用到的类
    JwtToken:自定义的token类,用以代替shiro原生的UsernamePasswordToken
    JwtRealm:自定义的Realm对象,处理token校验。
    ShiroDefaultSubjectFactory:自定义的subjectFactory,继承于DefaultSubjectFactory,用于生产subject对象,由于我们是无状态登录,所以重写该类弃用shiro内部的session。
    ShiroFilter:需要进行jwt认证的API接口经过的过滤器。
    ShiroCofig:Shiro的核心配置类,用于配置安全管理器(securityManager),授权过滤器(ShiroFilterFactoryBean)。

  • JwtToken 自定义的token类
public class JwtToken implements AuthenticationToken {

    private final String token;

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

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

    @Override
    public Object getCredentials() {
        return token;
    }
}
  • JwtRealm自定义的Realm对象,处理token校验。
/**
 * 处理token校验
 *
 * @author ding
 */
@Component
@RequiredArgsConstructor
public class JwtRealm extends AuthorizingRealm {

    private final JwtProvider jwtProvider;

    /**
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     * 不负责验证其他的token(UsernamePasswordToken)
     */

    @Override
    public boolean supports(AuthenticationToken token) {
        // 这个token就是从过滤器中传入的jwtToken
        return token instanceof JwtToken;
    }

    /**
     * 自定义授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String token = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 默认给一个user角色
        authorizationInfo.addRole("user");
        return authorizationInfo;
    }

    /**
     * 自定义认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String jwt = (String) authenticationToken.getPrincipal();
        // 解码token
        Claims claims = jwtProvider.decodeToken(jwt);
        if (claims == null) {
            throw new IncorrectCredentialsException("Authorization token is invalid");
        }
        // claims放入全局Subject中
        return new SimpleAuthenticationInfo(claims, jwt, "JwtRealm");
    }
}
  • ShiroDefaultSubjectFactory重写该类弃用shiro内部的session。
/**
 * 不创建shiro内部的session
 * @author ding
 */
public class ShiroDefaultSubjectFactory extends DefaultSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        // 不创建shiro内部的session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}
  • ShiroFilterAPI接口经过的过滤器
/**
 * 需要认证的url经过该过滤器
 *
 * @author ding
 */
@Slf4j
public class ShiroFilter extends AccessControlFilter {

    /**
     * 跨域支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 对跨域OPTIONS请求放行
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setStatus(HttpStatus.OK.value());
            return true;
        }
        return super.preHandle(request, response);
    }

    /**
     * 是否允许通过,因为是无状态所以默认不通过,去自动登陆,返回false,调用onAccessDenied方法
     * 这里getSubject方法实际上就是获得一个subject
     * 与原生shiro不同的地方在于没有对username和password进行封装
     * 直接使用jwt进行认证,login方法实际上就是交给Realm进行认证
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
        String token = ((HttpServletRequest) servletRequest).getHeader("token");
        if (token == null) {
            return false;
        }
        try {
            getSubject(servletRequest, servletResponse).login(new JwtToken(token));
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 自定义认证失败返回
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        httpResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
        ResponseResult<String> resp = ResponseResult.fail(ResponseResult.RespCode.UNAUTHORIZED);
        httpResponse.getWriter().write(JSON.toJSONString(resp));
        return false;
    }

}
  • ShiroCofig核心配置类
/**
 * shiro核心管理器:三大核心对象:Subject、SecurityManager、Realm
 *
 * @author ding
 */
@Configuration
@Slf4j
public class ShiroConfig {

    /**
     * 告诉shiro不创建内置的session
     */
    @Bean
    public SubjectFactory subjectFactory() {
        return new ShiroDefaultSubjectFactory();
    }

    /**
     * 创建安全管理器
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(JwtRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);
        // 关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    /**
     * 授权过滤器
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilter.setSecurityManager(securityManager);
        // 注册jwt过滤器,也就是将jwtFilter注册到shiro的Filter中,并在下面注册,指定除了login和logout之外的请求都先经过jwtFilter
        Map<String, Filter> filterMap = new HashMap<>(3) {
            {
                put("anon", new AnonymousFilter());
                put("jwt", new ShiroFilter());
                put("logout", new LogoutFilter());
            }
        };
        shiroFilter.setFilters(filterMap);
        // 拦截器
        Map<String, String> filterRuleMap = new LinkedHashMap<>(){
            {
                // 登录注册放行
                put("/login", "anon");
                put("/register", "anon");
                // swagger放行
                put("/swagger-ui.html", "anon");
                put("/swagger-resources", "anon");
                put("/v2/api-docs", "anon");
                put("/webjars/springfox-swagger-ui/**", "anon");
                put("/configuration/security", "anon");
                put("/configuration/ui", "anon");
                // 任何请求都需要经过jwt过滤器
                put("/**", "jwt");
            }
        };
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilter;
    }
}
  • 封装响应体ResponseResult,用于统一json响应(可选)
/**
 * 通用响应体
 *
 * @author qiding
 */
@Data
@Accessors(chain = true)
public class ResponseResult<T> implements Serializable {

    private static final long serialVersionUID = -1L;

    private Integer code;

    private String message;

    private T data;

    public ResponseResult(Integer code, String message, T data) {
        super();
        this.code = code;
        this.message = message;
        this.data = data;
    }

    private static <T> ResponseResult<T> build(Integer code, String message, T data) {
        return new ResponseResult<>(code, message, data);
    }

    public static <T> ResponseResult<T> ok() {
        return new ResponseResult<>(RespCode.OK.code, RespCode.OK.message, null);
    }

    public static <T> ResponseResult<T> ok(T data) {
        return build(RespCode.OK.code, RespCode.OK.message, data);
    }
    
    public static <T> ResponseResult<T> fail() {
        return fail(RespCode.ERROR.message);
    }

    public static <T> ResponseResult<T> fail(String message) {
        return fail(RespCode.ERROR, message);
    }

    public static <T> ResponseResult<T> fail(RespCode respCode) {
        return fail(respCode, respCode.message);
    }

    public static <T> ResponseResult<T> fail(RespCode respCode, String message) {
        return build(respCode.getCode(), message, null);
    }

    public enum RespCode {
        /**
         * 业务码
         */
        OK(20000, "请求成功"),
        MY_ERROR(20433, "自定义异常"),
        UNAUTHORIZED(20401, "未授权"),
        LOGIN_FAIL(20402, "账号或密码错误"),
        ERROR(20400, "未知异常");

        RespCode(int code, String message) {
            this.code = code;
            this.message = message;
        }

        private final int code;
        private final String message;

        public int getCode() {
            return code;
        }

        public String getMessage() {
            return message;
        }
    }
}

4. 开启跨域支持

  • 由于shiro是基于过滤器的,所以我们这里继承Filter ,进行跨域处理
/**
 * 由于shiro是基于过滤器的,所以我们这里继承Filter ,进行跨域处理
 *
 * @author ding
 */
@Component
@Slf4j
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws ServletException, IOException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", request.getMethod());
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "*");
        chain.doFilter(request, response);
    }

}

5. 登录注册实战

  • 编写User实体类,模拟一个用户
@Data
@Accessors(chain = true)
public class User {

    /**
     * id
     */
    private Long userId;

    /**
     * 账号
     */
    private String username;

    /**
     * 密码
     */
    private String password;
}
  • 封装shiro 工具类
/**
 * shiro工具类
 * 用于快速获取登录信息
 *
 * @author ding
 */
public class ShiroUtils {

    /**
     * md5盐
     */
    private static final String SALT = "xx.com";

    /**
     * 获取登录信息
     */
    public static Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    /**
     * 获取用户id
     *
     * @param <T> id类型
     */
    public static <T> T getUserId(Class<T> c) {
        Subject subject = getSubject();
        Claims claims = (Claims) subject.getPrincipal();
        return claims.get("userId", c);
    }

    /**
     * 密码md5加密
     *
     * @param password 密码
     */
    public static String md5(String password) {
        return new Md5Hash(password, SALT, 1024).toString();
    }

    /**
     * 密码比对
     *
     * @param password    未加密的密码
     * @param md5password 加密过的密码
     */
    public static boolean verifyPassword(String password, String md5password) {
        return new Md5Hash(password, SALT, 1024).toString().equals(md5password);
    }

    /**
     * 退出登录
     */
    public static void logout() {
        getSubject().logout();
    }

}
  • 编写测试API
/**
 * @author ding
 */
@RestController
@RequiredArgsConstructor
public class LoginController {

    private final JwtProvider jwtProvider;

    /**
     * 模拟一个数据库用户
     * 账号admin
     * 密码123456
     */
    private final static HashMap<String, User> USER_MAP = new LinkedHashMap<>() {
        {
            put("admin", new User()
                    .setUserId(1L)
                    .setUsername("admin")
                    .setPassword(ShiroUtils.md5("123456"))
            );
        }
    };

    /**
     * 登录
     */
    @PostMapping(value = "/login")
    public ResponseResult<String> login(@RequestParam("username") String username,
                                        @RequestParam("password") String password) {
        User user = USER_MAP.get(username);
        if (Objects.isNull(user)) {
            return ResponseResult.fail("用户不存在");
        }
        // 密码加密校验
        if (ShiroUtils.verifyPassword(password, user.getPassword())) {
            String token = jwtProvider.createToken(user.getUserId());
            return ResponseResult.ok(token);
        }
        return ResponseResult.fail("账号或密码错误");
    }

    /**
     * 注册
     */
    @PostMapping(value = "/register")
    public ResponseResult<String> register(@RequestParam("username") String username,
                                           @RequestParam("password") String password) {
        USER_MAP.put(username, new User()
                .setUserId(USER_MAP.size() + 1L)
                .setUsername(username)
                // 对密码进行加密保存
                .setPassword(ShiroUtils.md5(password)));
        return ResponseResult.ok("注册成功");
    }

    /**
     * 获取用户
     */
    @GetMapping("/getUser")
    public ResponseResult<User> getUser() {
        // 获取当前登录的用户id
        Long userId = ShiroUtils.getUserId(Long.class);
        User user = USER_MAP.values()
                .stream()
                .filter(u -> u.getUserId().equals(userId))
                .findFirst()
                .orElseThrow();
        return ResponseResult.ok(user);
    }

    /**
     * 退出登录
     */
    @GetMapping("/logout")
    public ResponseResult<String> logout() {
        SecurityUtils.getSubject().logout();
        return ResponseResult.ok("成功退出登录");
    }

}

6. 效果演示

  • 注册register
    在这里插入图片描述

  • 登录login
    在这里插入图片描述

  • 获取用户信息getUser,在请求头放入登录时获取的token
    在这里插入图片描述

7. 源码分享

本项目已收录

  • Springboot-cli开发脚手架,集合springboot、springcloud各种常用框架使用案例,完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来,并提供丰富的使用示例供使用者参考,帮助初学者快速上手。
  • 项目源码github地址
  • 项目源码国内gitee地址
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈小定

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值