整合SpringBoot
省去建表,数据库读取代码。
程序逻辑
在SpringBoot
中使用JWT
来做接口权限认证,安全框架依旧使用Shiro
- 我们
POST
用户名与密码到/login
进行登入,如果成功返回一个加密token
,失败的话直接返回401错误。
- 之后用户访问每一个需要权限的网址请求必须在
header
中添加Authorization
字段,例如Authorization: token
,token
为密钥。
- 后台会进行
token
的校验,如果不通过直接返回401。
Token加密说明
- 携带了
username
信息在 token 中。
- 设定了过期时间。
- 使用秘钥对
token
进行加密。
Token校验流程
服务端接收到token
之后,会逆向构造过程,decode
出JWT
的三个部分,这一步可以得到sign
的算法及 payload
,结合服务端配置的 secretKey
,可以再次进行 Signature
的生成得到新的 Signature
,与原有的 Signature
比对以验证 token
是否有效,完成用户身份的认证。
Maven
添加依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency>
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
|
JWT工具类
我们写一个简单的JWT
加密,校验工具,并且使用用户自己的密码充当加密密钥, 这样保证了token
即使被他人截获也无法破解。并且我们在token
中附带了username
信息,并且设置密钥1天就会过期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| public class JWTUtil {
private static final long EXPIRE_TIME = 24 * 60 * 1000;
public static boolean verify(String token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception e) { return false; } }
public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return ""; } }
private static String sign(String username, String secret) { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); }
public static Result generateUserInfo(UserInfo userInfo) { Map<String, Object> responseBean = new HashMap<>(2); String token = sign(userInfo.getUsername(), userInfo.getPassword()); responseBean.put("token", token); userInfo.setPassword(""); responseBean.put("userInfo", userInfo); return ResultFactory.buildSuccessResult(responseBean); }
}
|
创建JWTToken替换Shiro原生Token
Shiro
原生的 Token
中存在用户名和密码以及其他信息 [验证码,记住我],因为是前后端分离,服务器无需保存用户状态,所以不需要RememberMe
这类功能, 我们简单的实现下AuthenticationToken
接口即可
- 在
JWT
的 Token
中因为已将用户名和密码通过加密处理整合到一个加密串中,所以只需要一个 token
字段即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) { if(token.contains("Bearer")){ token = token.substring(7, token.length()); } this.token = token; }
@Override public Object getPrincipal() { return token; }
@Override public Object getCredentials() { return token; } }
|
创建JWTFilter实现前端请求统一拦截及处理
所有的请求都会先经过Filter
,所以我们继承官方的BasicHttpAuthenticationFilter
,并且重写鉴权的方法, 另外通过重写preHandle
,实现跨域访问。
代码的执行流程preHandle->isAccessAllowed->isLoginAttempt->executeLogin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static String LOGIN_SIGN = "Authorization";
@Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader(LOGIN_SIGN); return authorization != null; }
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String authorization = httpServletRequest.getHeader(LOGIN_SIGN); JWTToken token = new JWTToken(authorization); getSubject(request, response).login(token); return true; }
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){ if (isLoginAttempt(request, response)) { try { executeLogin(request, response); } catch (Exception e) { String msg = e.getMessage(); this.response401(response, msg); return false; } } return true; }
@Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { this.sendChallenge(request, response); return false; }
@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = WebUtils.toHttp(request); HttpServletResponse httpServletResponse = WebUtils.toHttp(response); httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); }
private void response401(ServletResponse response, String msg) { HttpServletResponse httpServletResponse = WebUtils.toHttp(response); httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); try (PrintWriter out = httpServletResponse.getWriter()) { Result result = ResultFactory.buildUnauthorizedResult("无权访问(Unauthorized):" + msg); JSONObject jsonObject = JSONUtil.parseObj(result); out.append(jsonObject.toString()); } catch (IOException e) { throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage()); } }
}
|
实现Realm
realm
的用于处理用户是否合法的这一块,需要我们自己实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| public class ShiroRealm extends AuthorizingRealm {
private UserRepository userRepository;
@Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; }
@Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String username = JWTUtil.getUsername(principalCollection.toString()); UserInfo userInfo = userRepository.findByUsername(username); List<String> permissionList = new ArrayList<>(); List<String> roleNameList = new ArrayList<>(); Set<SysRole> roleSet = userInfo.getRoleList();
if (CollectionUtils.isNotEmpty(roleSet)) { for (SysRole role : roleSet) { roleNameList.add(role.getName()); Set<SysPermission> permissionSet = role.getPermissions(); if (CollectionUtils.isNotEmpty(permissionSet)) { for (SysPermission permission : permissionSet) { permissionList.add(permission.getUrl()); } } } } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionList); info.addRoles(roleNameList); return info; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); String userName = JWTUtil.getUsername(token); String secert = userRepository.getCredentials(userName);
if (StringUtils.isBlank(token) || !JWTUtil.verify(token, userName, secert)) { throw new AuthenticationException("token校验不通过"); }
return new SimpleAuthenticationInfo(token, token, "shiroRealm"); } }
|
配置Shiro
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| public class ShiroConfig {
@Primary @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager);
LinkedHashMap<String, Filter> filters = new LinkedHashMap<>(); filters.put("jwt", new JWTFilter()); shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
@Primary @Bean public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm());
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO);
return securityManager; }
@Primary @Bean public ShiroRealm shiroRealm() { return new ShiroRealm(); } }
|
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @PostMapping("/login") public Result login(@RequestBody Map<String, String> payload){ String username = payload.get("username"); String password = payload.get("password"); final String errorMessage = "用户名或密码错误";
UserInfo userInfo = userRepository.findByUsername(username); if(userInfo == null){ return ResultFactory.buildFailResult(errorMessage); } if(!Argon2Util.verify(userInfo.getPassword(), password)){ return ResultFactory.buildFailResult(errorMessage); } return JWTUtil.generateUserInfo(userInfo); }
@GetMapping("/123") @RequiresAuthentication public Result test(){ System.out.println(111); return ResultFactory.buildSuccessResult("成功"); }
|
参考:
SpringBoot系列 - 集成JWT实现接口权限认证 | 飞污熊博客
Shiro + JWT + Spring Boot Restful 简易教程 - 沧海月明
SpringBoot+Shiro+Vue前后端分离项目通过JWT实现自动登录 | asing1elife’s blog