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

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


Springboot-cli 开发脚手架系列


文章目录

  • Springboot-cli 开发脚手架系列
  • 简介
    • 1. Springboot实战完整教程
    • 2. 封装jwt工具
    • 3. shiro和JWT整合
    • 4. 开启跨域支持
    • 5. 登录注册实战
    • 6. 效果演示
    • 7. 源码分享

简介

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

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

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

  • 这种基于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依赖
       <dependency>     <groupId>org.apache.shiro</groupId>     <artifactId>shiro-core</artifactId>     <version>1.9.0</version> </dependency>  <dependency>     <groupId>org.apache.shiro</groupId>     <artifactId>shiro-spring</artifactId>     <version>1.9.0</version> </dependency>  <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt</artifactId>     <version>0.9.1</version> </dependency>  <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: 9999jwt:  # 密钥  secret: xxxxx.xxxx.xxxx  # 有效期(秒)  expire: 86400

2. 封装jwt工具

  • TokenProvider 用于生成及校验token
/** * token管理 * * @author Ding */@Slf4j@Componentpublic 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@RequiredArgsConstructorpublic 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 */@Slf4jpublic 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@Slf4jpublic 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@Slf4jpublic 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  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@RequiredArgsConstructorpublic 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
    Springboot实战:Shiro+jwt前后端分离超详细教程(附git源码下载)

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

  • 获取用户信息getUser,在请求头放入登录时获取的token
    Springboot实战:Shiro+jwt前后端分离超详细教程(附git源码下载)

7. 源码分享

本项目已收录

  • Springboot-cli开发脚手架,集合springboot、springcloud各种常用框架使用案例,完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来,并提供丰富的使用示例供使用者参考,帮助初学者快速上手。
  • 项目源码github地址
  • 项目源码国内gitee地址