1. 开始之前
1.1 技术选型
选用SpringBoot+Shiro+JWT
实现登录认证,结合Redis
服务实现token
的续签,前端选用Vue
动态构造路由及更细粒度的操作权限控制。
- 前后端分离项目中,我们一般采用的是无状态登录:服务端不保存任何客户端请求者信息,客户端需要自己携带着信息去访问服务端,并且携带的信息可以被服务端辨认。
- 而
Shiro
默认的拦截跳转都是跳转url
页面,拦截校验机制恰恰使用的session
;而前后端分离后,后端并无权干涉页面跳转。
- 因此前后端分离项目中使用
Shiro
就需要对其进行改造,我们可以在整合Shiro
的基础上自定义登录校验,继续整合JWT
(或者oauth2.0等),使其成为支持服务端无状态登录,即token
登录。
- 在
Vue
项目中,只需要根据登录用户的权限信息动态的加载路由列表就可以动态的构造出访问菜单。
1.2 整体流程
- 首次通过
post
请求将用户名与密码到login
进行登入,登录成功后返回token
;
- 每次请求,客户端需通过
header
将token
带回服务器做JWT Token
的校验;
- 服务端负责
token
生命周期的刷新,用户权限的校验;

2. SpringBoot整合Shiro+JWT
这里贴出主要逻辑,源码请移步文章末尾获取。
- 数据表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user( id INT AUTO_INCREMENT COMMENT '用户ID', account VARCHAR(30) NOT NULL COMMENT '用户名', PASSWORD VARCHAR(50) COMMENT '用户密码', salt VARCHAR(8) COMMENT '随机盐', nickname VARCHAR(30) COMMENT '用户昵称', roleId INT COMMENT '角色ID', createTime DATE COMMENT '创建时间', updateTime DATE COMMENT '更新时间', deleteStatus VARCHAR(2) DEFAULT '1' COMMENT '是否有效:1有效,2无效', CONSTRAINT sys_user_id_pk PRIMARY KEY(id), CONSTRAINT sys_user_account_uk UNIQUE(account) ); COMMIT;
|
- pom.xml
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version> </dependency>
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.2</version> </dependency>
|
shiro
配置类:构建securityManager
环境,及配置shiroFilter
并将jwtFilter
添加进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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| @Configuration public class ShiroConfig { @Bean("securityManager") public DefaultWebSecurityManager getManager(MyRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; }
@Bean("shiroFilter") public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/register", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/unauthorized", "anon");
Map<String, Filter> filterMap = new HashMap<>(1); filterMap.put("jwt", new JwtFilter()); factoryBean.setFilters(filterMap);
filterChainDefinitionMap.put("/**", "jwt"); factoryBean.setUnauthorizedUrl("/unauthorized");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; }
@Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
|
- 自定义
Realm
:继承AuthorizingRealm
类,在其中实现登陆验证及权限获取的方法。
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
| @Slf4j @Component("MyRealm") public class MyRealm extends AuthorizingRealm { private SysService sysService; @Autowired public void setSysService(SysService sysService) { this.sysService = sysService; }
@Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { log.info("————————身份认证——————————"); String token = (String) auth.getCredentials(); if (null == token || !JwtUtil.isVerify(token)) { throw new AuthenticationException("token无效!"); } String account = JwtUtil.parseTokenAud(token); User user = sysService.selectByAccount(account); if (null == user) { throw new AuthenticationException("用户不存在!"); } return new SimpleAuthenticationInfo(user, token,"MyRealm"); }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("————权限认证 [ roles、permissions]————"); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); return simpleAuthorizationInfo; } }
|
- 鉴权登录过滤器:继承
BasicHttpAuthenticationFilter
类,该拦截器需要拦截所有请求除(除登陆、注册等请求),用于判断请求是否带有token
,并获取token
的值传递给shiro
的登陆认证方法作为参数,用于获取token
;
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
| @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { unauthorized(response); return false; } }
@Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String authorization = httpServletRequest.getHeader("X-Token"); JwtToken token = new JwtToken(authorization); getSubject(request, response).login(token); return true; }
private void unauthorized(ServletResponse resp) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.sendRedirect("/unauthorized"); } catch (IOException e) { log.error(e.getMessage()); } }
@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) 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); } }
|
JwtToken
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class JwtToken implements AuthenticationToken { private String token; JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
|
JWT
工具类:利用登陆信息生成token
,根据token
获取username
,token
验证等方法。
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
| public class JwtUtil { private static final long EXPIRE_TIME = 30 * 60 * 1000; private static final String TOKEN_SECRET = "zhengchao";
public static String createToken(User user) { try { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); Map<String, Object> header = new HashMap<>(2); header.put("typ", "JWT"); header.put("alg", "HS256"); return JWT.create() .withHeader(header) .withClaim("aud", user.getAccount()) .withClaim("uid", user.getId()) .withExpiresAt(date) .sign(algorithm); } catch (Exception e) { e.printStackTrace(); return null; } }
public static boolean isVerify(String token){ try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); return true; } catch (Exception e){ return false; } }
public static int parseTokenUid(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("uid").asInt(); }
public static String parseTokenAud(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("aud").asString(); }
public static Date paraseExpiresTime(String token){ DecodedJWT jwt = JWT.decode(token); return jwt.getExpiresAt(); } }
|
- MD5加密工具类
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
| public class Md5Util {
public static String md5(String s) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(s.getBytes("utf-8")); return toHex(bytes); } catch (Exception e) { throw new RuntimeException(e); } }
public static String salt(){ UUID uuid = UUID.randomUUID(); String[] arr = uuid.toString().split("-"); return arr[0]; }
private static String toHex(byte[] bytes) { final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); StringBuilder ret = new StringBuilder(bytes.length * 2); for (int i=0; i<bytes.length; i++) { ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]); ret.append(HEX_DIGITS[bytes[i] & 0x0f]); } return ret.toString(); } }
|
3. 注册与登录主要逻辑
这里只贴出主要逻辑,DAO
和Mapper
映射可查看源码,源码请移步文章末尾获取。
- 登录
Controller
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
| @RestController public class SysApi {
private SysService sysService; @Autowired public void setSysService(SysService sysService) { this.sysService = sysService; }
@PostMapping("/register") public ResponseVo<String> register(String account, String password) { return sysService.register(account, password); }
@PostMapping("/login") public ResponseVo<String> login(String account, String password) { return sysService.login(account, password); }
@GetMapping("/unauthorized") public ResponseVo unauthorized(HttpServletRequest request) { return new ResponseVo(-1, "Token失效请重新登录!"); } }
|
- Service
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
| public interface SysService {
ResponseVo<String> register(String account, String password);
ResponseVo<String> login(String account, String password);
User selectByAccount(String account); }
@Service public class SysServiceImpl implements SysService {
private SysDao sysDao;
@Autowired public void setSysDao(SysDao sysDao) { this.sysDao = sysDao; }
@Override public ResponseVo<String> register(String account, String password) { User user = sysDao.selectByAccount(account); if(user!=null) { return new ResponseVo<>( -1, "用户名被占用"); } user = new User(); user.setAccount(account); String salt = Md5Util.salt(); String md5Password = Md5Util.md5(password+salt); user.setPassword(md5Password); user.setSalt(salt); user.setCreatetime(new Date()); int row = sysDao.insertSelective(user); if(row>0) { String token = JwtUtil.createToken(user); return new ResponseVo<>(0,"注册成功", token); }else { return new ResponseVo<>( -1, "注册失败"); } }
@Override public ResponseVo<String> login(String account, String password) { User user = sysDao.selectByAccount(account); if(user!=null) { String salt = user.getSalt(); String md5Password = Md5Util.md5(password+salt); String dbPassword = user.getPassword(); if(md5Password.equals(dbPassword)) { String token = JwtUtil.createToken(user); return new ResponseVo<>(0,"登录成功", token); } } return new ResponseVo<>( -1, "登录失败"); }
@Override public User selectByAccount(String account) { return sysDao.selectByAccount(account); } }
|
- 统一接口返回格式
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
| public class ResponseVo<T> { private int code; private String msg; private T data; public ResponseVo() {} public ResponseVo(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseVo(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
|
注:这里的登录认证逻辑在github
源码tag
的V1.0
中,后续版本再加入Token
续签和shiro
前后端权限管理等。
源码地址: https://github.com/chaooo/springboot-vue-shiro.git
仅下载认证逻辑源码:
git clone --branch V1.0 https://github.com/chaooo/springboot-vue-shiro.git