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,是目前最流行的跨域身份验证解决方案。 -
验证流程
-
这种基于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); }}
ShiroFilter
API接口经过的过滤器
/** * 需要认证的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
-
登录
login
-
获取用户信息
getUser
,在请求头放入登录时获取的token
7. 源码分享
本项目已收录
- Springboot-cli开发脚手架,集合springboot、springcloud各种常用框架使用案例,完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来,并提供丰富的使用示例供使用者参考,帮助初学者快速上手。
- 项目源码github地址
- 项目源码国内gitee地址