springboot+jwt+shiro集成实现前后端分离的登录认证和拦截
2020-01-14 22:02
1191 查看
这里是搞前端的菜鸡一个,在公司实习的时候组长给派了个任务,让实现系统的登录认证功能。好在以前做课设的时候有做过相关的功能,所以也不算是为难我。根据我本人的经验选了jwt+shiro+redis来实现登录认证的功能。顺带一提前端是react+umi+dva.
一、思路
- 使用token作为验证用户是否登录的唯一标识。
- 用户登录,通过用户名和密码认证,在后端用jwt生成一个token,并将token存储在redis中,拿到这个token由后端响应操作将它放在浏览器的cookie里面,返回结果给前端。
- 前端每次请求都会(被动)带上这个token,因为这时候浏览器的cookie里面已经有了token的记录了,每次请求后端都会检查cookie里面的token,然后进行验证,验证成功了才会返回请求的资源,否则返回错误结果,前端再进行相应的处理。
二、实现
1.数据库表
数据库的设计是考虑到RBAC的用户-角色-权限方式来设计的,虽然这里主要是做登录认证的功能,但是后续还想做权限管理的功能,所以表还是要按规矩设计。主要涉及到以下几个表。
- 用户表user
- 用户–角色表user_role
- 角色表role
- 角色–权限表role_permission
- 权限表
2.shiro引入和配置
在这里按照前端发起一个请求,然后shiro的拦截器拦截到请求,返回响应的一个过程来依次列出涉及到的文件。因为我自己就是摸索这个过程花费了比较多的时间。卑微前端踩坑太难了QAQ
首先在pom.xml中引入shiro,其他基本的springboot的依赖就不多说啦。
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
ShiroConfiguration.java
/** * shiro配置文件 */ @Configuration public class ShiroConfiguration { private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class); //从配置文件里面读取是否需要启动登录认证的开关,默认true @Value("${jwt.auth}") private boolean auth; //配置拦截器 @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //启用认证 String openAuth = auth ? "auth" : "anon"; //自定义过滤器链 Map<String, Filter> filters = new HashMap<>(); //指定拦截器处理 filters.put("auth", new AuthFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterMap = new LinkedHashMap<>(); //登录请求不拦截 filterMap.put("/user/login", "anon"); //登录页面需要用到的接口,不拦截 filterMap.put("/user/fetchCurrentUser", "anon"); //拦截所有接口请求,做权限判断 filterMap.put("/**", openAuth); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); logger.info("Shiro拦截器工厂类注入成功"); return shiroFilterFactoryBean; } // SecurityManager 安全管理器;Shiro的核心 @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm()); return securityManager; } //自定义身份认证realm @Bean public AuthRealm userRealm() { return new AuthRealm(); } @Bean("lifecycleBeanPostProcessor") //管理shiro生命周期 public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } //Shiro注解支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
- 文件里的
auth
变量是因为大家正在开发过程中,每每请求认证会导致不便,要求给一个能够控制是否认证的开关放在配置文件,一般就是application.yml或者application.properties,我随便起了个名字叫jwt.auth
,直接放在配置文件里面就好了,然后使用@Value
注解去配置文件里读。 - 注意到
lifecycleBeanPostProcessor()
方法定义为了静态的,这里是因为要去配置文件里面读取auth变量,如果不定义为静态的话会出现因为springboot扫描bean的顺序先后而导致这个文件里读取不到auth的值的问题,所以改成static了来绕过这个问题。 - 在这个shiro的配置文件里面定义需要开放的接口,标注为anon,表示访问这些接口不需要经过认证,到时候前端请求什么就直接给什么了。需要拦截的接口,标注为auth,表示请求这些接口都要经过认证流程。比如我这里的就是开放了登录页面需要用到的接口和用户登录的时候要用的接口。其他的所有接口请求都需要经过认证。
- 要注意的一个点是,shiro的拦截器链是顺序判断的,写在后面的会有叠加前面的效果,所以一般是开放的接口写在前面,不开放的接口写在后面。
filters.put("auth", new AuthFilter());
这一句就是给认证标签auth指定了一个拦截器,我们的主要功能就是在这个拦截器里面实现。
AuthFilter.java
/** * 实现自定义的认证拦截器,接收传过来的token,实现前后端分离的权限认证 */ public class AuthFilter extends AuthenticatingFilter { private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class); private Result responseResult = ResultUtils.forbiddenError(AuthConstant.AUTHENTICATE_FAIL); @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { return null; } /** * 在这里拦截所有请求 * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request); if (!StringUtils.isBlank(token)){ try { this.executeLogin(request, response); } catch (Exception e) { // 应用异常 logger.info(e.getMessage()); responseResult = ResultUtils.forbiddenError(e.getMessage()); return false; } } else { // cookie中未检查到token或token为空 HttpServletRequest httpServletRequest = WebUtils.toHttp(request); String httpMethod = httpServletRequest.getMethod(); String requestURI = httpServletRequest.getRequestURI(); responseResult = ResultUtils.forbiddenError(AuthConstant.TOKEN_BLANK); logger.info("请求 {} 的Token为空 请求类型 {}", requestURI, httpMethod); return false; } return true; } /** * 请求失败拦截,请求终止,不进行转发直接返回客户端拦截结果 * @param request * @param response * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception{ HttpServletResponse httpServletResponse = (HttpServletResponse)response; httpServletResponse.setContentType("application/json; charset=utf-8"); httpServletResponse.setCharacterEncoding("UTF-8"); String result = JsonConvertUtil.objectToJson(responseResult); httpServletResponse.getWriter().print(result); return false; } /** * 用户存在,执行登录认证 * @param request * @param response * @return * @throws Exception */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request); AuthTokenVo jwtToken = new AuthTokenVo(token); // 提交给AuthRealm进行登录认证 getSubject(request, response).login(jwtToken); return true; } }
- 所有待认证的请求进入了这个拦截器里面,进入
isAccessAllowed()
方法,进行初步判断,首先拿到浏览器cookie中的token,进行判空,若不为空执行下一步executeLogin()
来执行真正的登录token认证。 - 在
isAccessAllowed()
返回结果为true就说明认证通过了,前端可以尽情请求资源啦。出现任何错误都会返回false,就会跳到onAccessDenied()
方法,处理拦截结果返回给前端。
AuthRealm.java
/** * 自定义安全数据Realm */ public class AuthRealm extends AuthorizingRealm { private static final transient Logger logger = LoggerFactory.getLogger(AuthRealm.class); @Autowired private UserService userService; /** * 重写,绕过身份令牌异常导致的shiro报错 * @param authenticationToken * @return */ @Override public boolean supports(AuthenticationToken authenticationToken){ return authenticationToken instanceof AuthTokenVo; } /** * 执行授权逻辑 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){ logger.info("用户角色权限认证"); //获取用户登录信息 UserVo userVo = (UserVo)principals.getPrimaryPrincipal(); //添加角色和权限 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); for(RoleVo role : userVo.getRoleVoList()){ authorizationInfo.addRole(role.getRoleName()); for(PermissionVo permissionVo : role.getPermissionVoList()){ authorizationInfo.addStringPermission(permissionVo.getPermissionName()); } } return authorizationInfo; } /** * 执行认证逻辑 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{ logger.info("执行认证逻辑"); //获得token String token = (String)authenticationToken.getCredentials(); //获得token中的用户信息 String user = JwtAuthenticator.getUsername(token); //判空 if(StringUtils.isBlank(user)){ throw new AuthenticationException(AuthConstant.TOKEN_BLANK); } try{ //查询用户是否存在 List<UserVo> userVo = userService.login(new UserQuery(user, null)); if(userVo.size() <= 0){ throw new AuthenticationException(AuthConstant.TOKEN_INVALID); //token过期 }else if(!(JwtAuthenticator.verifyToken(token, user, userVo.get(0).getLoginPWD()))){ throw new AuthenticationException(AuthConstant.TOKEN_EXPIRE); } }catch (Exception e){ throw e; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( token, token, "auth_realm"); return authenticationInfo; } }
- 这里主要进行权限管理和登录认证的两个主要功能,重写了
AuthorizingRealm.java
的两个方法。登录认证这一块的功能是doGetAuthenticationInfo(AuthenticationToken authenticationToken)
来实现。 - 这里开始用到了jwt的token验证方法,主要是判断token是否过期,然后从token中读取出用户的信息,再与数据库对比是否正确。
出现任何错误都抛出给上一层AuthFilter.java
进行相应的处理。直到返回正确的认证信息,说明认证成功。
AuthConstant.java
/** * 权限相关的常量 */ public class AuthConstant { /** * cookie中存储的token字段名 */ public final static String COOKIE_TOKEN_NAME = "Authorization"; /** * token有效时间 时*分*秒*1000L */ public final static Long EXPIRE_TIME = 3*60*1000L;//先设置3分钟 //登录认证结果,返回给前端 public final static String UNKNOWN_ACCOUNT = "登录失败, 用户不存在。"; public final static String WRONG_PASSWORD = "登录失败,密码错误。"; public final static String TOKEN_BLANK = "验证失败,token为空,请登录。"; public final static String TOKEN_INVALID = "验证失败,token错误。"; public final static String TOKEN_EXPIRE = "验证失败,token过期,请重新登录。"; public final static String AUTHENTICATE_FAIL = "无访问权限,请尝试登录或联系管理员。"; }
认证链中用到的常量。
2.jwt(Json Web Token)引入和配置
在pom.xml中引入jwt的依赖
<!--jwt(Json Web Token)--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.7.0</version> </dependency>
写一个jwt的工具类来做token的效验
JwtAuthenticator.java
public class JwtAuthenticator { /** * 校验token是否正确 * @param token * @param username * @param secret * @return */ public static boolean verifyToken(String token, String username, String secret){ // 根据密码生成JWT校验器 try{ Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username",username) .build(); // 校验token,这里是jwt的内部实现,可能会抛出错误(token错误或过期等),同样将错误抛回给上层 DecodedJWT jwt = verifier.verify(token); return true; }catch (Exception e){ return false; } } /** * 获得token中的用户信息,无需解密 * @param token * @return */ public static String getUsername(String token){ try{ DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); }catch (JWTDecodeException e){ return null; } } /** * 生成签名 * @return */ public static String sign(UserVo userVo, Date expireTime) { String secret = userVo.getLoginPWD(); String userName = userVo.getUserName(); Algorithm algorithm = Algorithm.HMAC256(secret); // 附带username信息和过期信息 return JWT.create() .withClaim("username", userName) .withExpiresAt(expireTime) .sign(algorithm); } /** * 从cookie中获取token * @param httpServletRequest * @return */ public static String getRequestToken(HttpServletRequest httpServletRequest){ String token = ""; Cookie[] cookies = httpServletRequest.getCookies(); if(cookies != null){ for(Cookie ck : cookies){ if(StringUtils.equals(AuthConstant.COOKIE_TOKEN_NAME, ck.getName())){ token = ck.getValue(); break; } } } return token; } /** * 编辑浏览器cookie * @param response * @param tokenValue */ public static void editCookieToken(ServletResponse response, String tokenValue){ HttpServletResponse httpServletResponse = (HttpServletResponse)response; Cookie cookie = new Cookie(AuthConstant.COOKIE_TOKEN_NAME, tokenValue); cookie.setPath("/"); cookie.setHttpOnly(true);//前端不可读cookie //跨域向前端写cookie httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletResponse.getHeader("Origin")); httpServletResponse.addCookie(cookie); } }
- 考虑到web安全问题,由后端向前端写cookie并设置Http-Only阻止前端读cookie,读取cookie也是后端直接读。
verifyToken(String token, String username, String secret)
是实现验证token的主要方法,token会是一串很长的码然后jwt会自己从里面读取出用户信息,并验证token是否有效。sign(UserVo userVo, Date expireTime)
方法是实现在用户登录的时候,根据用户信息和我们规定的过期时间来生成一个独一无二的签名(token),我们会将这个token返回给前端,作为用户在前端访问后台接口的唯一凭证。
AuthTokenVo.java
public class AuthTokenVo implements AuthenticationToken { private String token; public AuthTokenVo(String token){ this.token = token; } @Override public Object getPrincipal(){ return token; } @Override public Object getCredentials(){ return token; } }
这个是token的实体类,需要实现AuthenticationToken接口。
3.用户登录功能
用户登录就是普通的功能啦,跟shiro没什么关系,因为这里是用token来作为凭证的,然后shiro中设置了开放用户登录的接口,重要的一点是,登录成功的话会根据用户身份生成一个token返回给前端。
UserController.java
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * 登录 * @param userQuery * @param response * @return * @throws Exception */ @PostMapping("/login") public Result login(@RequestBody UserQuery userQuery, ServletResponse response) throws Exception{ List<UserVo> userVo = userService.login(userQuery); if(userVo.size() == 0){ //账号不存在 return ResultUtils.dataNotFoundError(AuthConstant.UNKNOWN_ACCOUNT); }else if(!userVo.get(0).getLoginPWD().equals(userQuery.getLoginPWD())){ //密码错误 return ResultUtils.error(AuthConstant.WRONG_PASSWORD); }else{ //通过认证, 生成签名 String token = userService.saveToken(userVo.get(0)); //token写入前端cookie JwtAuthenticator.editCookieToken(response, token); return ResultUtils.success(userVo); } } /** * 注销 * @param request * @param response * @return */ @PostMapping("/logout") public Result logout(ServletRequest request, ServletResponse response){ String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request); String userName = JwtAuthenticator.getUsername(token); List<UserVo> userVo = userService.qryUserByUserName(userName); userService.deleteToken(userVo.get(0)); //前端token置空 JwtAuthenticator.editCookieToken(response, ""); return ResultUtils.success(); } /** * 查询当前用户,通过token解密查询 * @param request * @return */ @PostMapping("/fetchCurrentUser") public Result fetchCurrentUser(ServletRequest request){ String token = JwtAuthenticator.getRequestToken((HttpServletRequest)request); String userName = JwtAuthenticator.getUsername(token); List<UserVo> userVo = userService.qryUserByUserName(userName); if(userVo.size() <= 0){ return ResultUtils.dataNotFoundError(AuthConstant.UNKNOWN_ACCOUNT); } return ResultUtils.success(userVo.get(0)); } }
UserService.java
@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private JedisUtil jedisUtil; @Override //登录,根据用户信息查询用户并关联角色和权限 public List<UserVo> login(UserQuery userQuery){ Map<Long, UserVo> userVoMap = new HashMap<Long, UserVo>(); //根据查询条件查出用户 List<UserDto> userDtoList = new ArrayList<UserDto>(); if(userQuery.getUserName()!=null && !userQuery.getUserName().equals("")){ userDtoList = userDao.qryUserByUserName(userQuery.getUserName()); } if(userDtoList.size() <= 0){ return new ArrayList<UserVo>(userVoMap.values()); } List<Long> userIds = new ArrayList<Long>(); for(UserDto u : userDtoList){ userIds.add(u.getUserId()); userVoMap.put(u.getUserId(), new UserVo(u)); } List<Long> roleIds = new ArrayList<Long>(); Map<Long, RoleVo> roleVoMap = new HashMap<Long, RoleVo>(); // 查出用户的全部角色,然后添加到用户的角色列表中 List<RoleDto> roleDtoList = roleDao.qryRoleByUserIds(userIds); for(RoleDto role : roleDtoList) { roleIds.add(role.getRoleId()); RoleVo roleVo = new RoleVo(role); roleVoMap.put(role.getRoleId(), roleVo); UserVo userVo = userVoMap.get(role.getUserId()); if (null == userVo) { continue; } if (null == userVo.getRoleVoList()) { userVo.setRoleVoList(new ArrayList<RoleVo>()); } userVo.getRoleVoList().add(roleVo); } if(roleIds.size() <= 0){ return new ArrayList<UserVo>(userVoMap.values()); } // 查出角色对应的资源权限,添加到对应的角色中 List<PermissionDto> permissionDtos = permissionDao.qryPermissionByRoleIds(roleIds); for(PermissionDto permission : permissionDtos) { RoleVo roleVo = roleVoMap.get(permission.getRoleId()); if (null == roleVo) { continue; } List<PermissionVo> permissionVoList = roleVo.getPermissionVoList(); if (null == permissionVoList) { permissionVoList = new ArrayList<PermissionVo>(); } permissionVoList.add(DataTranslationUtils.trans(permission, PermissionVo.class)); roleVo.setPermissionVoList(permissionVoList); } return new ArrayList<UserVo>(userVoMap.values()); } //根据用户名查询用户,不关联角色 @Override public List<UserVo> qryUserByUserName(String userName) { List<UserDto> userDtoList = userDao.qryUserByUserName(userName); return DataTranslationUtils.trans(userDtoList, UserVo.class); } //处理token,调用生成token的方法,返回token,在这里引入了redis,如果有必要的话想将token保存到redis里面。 @Override public String saveToken(UserVo userVo){ try{ Date setTime = new Date(); Date expireTime = new Date(); expireTime.setTime(setTime.getTime() + AuthConstant.EXPIRE_TIME); String token = JwtAuthenticator.sign(userVo, expireTime); //如果JedisPoll存在,将token存储起来。 if(jedisUtil.getJedisPool() != null){ TokenDto tokenDto = new TokenDto(); tokenDto.setUserId(userVo.getUserId()); tokenDto.setToken(token); tokenDto.setUpdateTime(setTime); tokenDto.setExpireTime(expireTime); jedisUtil.set( String.valueOf(userVo.getUserId()).getBytes(), SerializeUtil.serialize(tokenDto), JedisConfig.database); } return token; }catch (Exception e){ } return null; } //删除token,主要是用户注销的时候,设置token立马过期,并删除Jedis里面存储的token @Override public void deleteToken(UserVo userVo) { Date currentTime = new Date(); JwtAuthenticator.sign(userVo, currentTime); if(jedisUtil.getJedisPool() != null){ jedisUtil.del(JedisConfig.database,String.valueOf(userVo.getUserId()).getBytes()); } } }
- 看了一些资料,把jwt生成的token存储到redis里面其实是没有必要的,因为jwt本就是实现无存储token的一种方案,因此redis这一部分就不贴出来了。实际上如果想实现将token存储到redis应该脱离jwt,自己实现一个token的加密和解密,并实现token的有效时间的管理,将这些东西放在redis里面。
- 查询数据库的方式我这边是直接用sql语句来查的,当然也可以借助JPA或者myBatis等框架来查询,Dao层的代码就不贴出来啦。
4.相关工具类
StringUtil.java
/** * String工具 */ public class StringUtil { /** * 定义下划线 */ private static final char UNDERLINE = '_'; /** * String为空判断(不允许空格) * @param str * @return boolean */ public static boolean isBlank(String str) { return str == null || "".equals(str.trim()); } /** * String不为空判断(不允许空格) * @param str * @return boolean */ public static boolean isNotBlank(String str) { return !isBlank(str); } /** * Byte数组为空判断 * @param bytes * @return boolean */ public static boolean isNull(byte[] bytes) { // 根据byte数组长度为0判断 return bytes == null || bytes.length == 0; } /** * Byte数组不为空判断 * @param bytes * @return boolean */ public static boolean isNotNull(byte[] bytes) { return !isNull(bytes); } }
JsonConvertUtil.java
/** * Json和Object的转换工具 */ public class JsonConvertUtil { /** * JSON 转 Object */ public static <T> T jsonToObject(String pojo, Class<T> clazz) { return JSONObject.parseObject(pojo, clazz); } /** * Object 转 JSON */ public static <T> String objectToJson(T t){ return JSONObject.toJSONString(t); } }
ResultUtils.java
/** * 接口返回结果工具类 */ public class ResultUtils { public static Result success(Object object) { return new Result() .setCode(ResultEnum.SUCCESS.getCode()) .setMessage(ResultEnum.SUCCESS.getMessage()) .setData(object); } public static Result success() { return success(null); } public static Result error(String errMsg) { return error(ResultEnum.FAIL.getCode(), ResultEnum.FAIL.getMessage(), errMsg); } public static Result dataNotFoundError(String errMsg) { return error(ResultEnum.DATA_NOT_FOUND.getCode(), ResultEnum.DATA_NOT_FOUND.getMessage(), errMsg); } public static Result unauthorizedError(String errMsg) { return error(ResultEnum.UNAUTHORIZED.getCode(), ResultEnum.UNAUTHORIZED.getMessage(), errMsg); } public static Result forbiddenError(String errMsg) { return error(ResultEnum.FORBIDDEN.getCode(), ResultEnum.FORBIDDEN.getMessage(), errMsg); } public static Result paramNotVaildError(String errMsg) { return error(ResultEnum.PARAM_NOT_VALID.getCode(), ResultEnum.PARAM_NOT_VALID.getMessage(), errMsg); } public static Result paramTypeConversionError(String errorMsg) { return error(ResultEnum.TYPE_CONVERTION_ERROR.getCode(), ResultEnum.TYPE_CONVERTION_ERROR.getMessage(), errorMsg); } public static Result numberFormatError(String errorMsg) { return error(ResultEnum.NUMBER_FORMAT_ERROR.getCode(), ResultEnum.NUMBER_FORMAT_ERROR.getMessage(), errorMsg); } public static Result internalServerError(String errorMsg) { return error(ResultEnum.INTERNAL_SERVER_ERROR.getCode(), ResultEnum.INTERNAL_SERVER_ERROR.getMessage(), errorMsg); } public static Result interfaceNotFoundError(String errorMsg) { return error(ResultEnum.NOT_FOUND.getCode(), ResultEnum.NOT_FOUND.getMessage(), errorMsg); } private static Result error(Integer code, String message, String errMsg) { return new Result() .setCode(code) .setMessage(message) .setData(errMsg); } }
5.结果展示
-
未登录
-
登录成功,返回对象
查看浏览器cookie
-
正常请求
-
token过期
-
注销
做这个功能的过程中参考了好多大神的项目代码,在这里表示感谢(链接找不到了)。给自己写了一周的成果做一点记录,不然过段时间又忘了,还是自己太菜了,以后还是要多学习,每天都要进步一点点!
- 点赞 1
- 收藏
- 分享
- 文章举报
相关文章推荐
- spring boot 前后端分离整合shiro(五)整合redis并实现并发登录控制
- shiro,基于springboot,基于前后端分离,从登录认证到鉴权,从入门到放弃
- SpringBoot+Shiro+Jwt实现登录认证
- SpringBoot整合Shiro实现登录认证的方法
- vue+springboot前后端分离实现单点登录跨域问题解决方法
- 在前后端分离的SpringBoot项目中集成Shiro权限框架
- shiro 权限认证框集成到spring中,实现登陆与权限拦截
- spring集成shiro实现权限认证和自动登录
- Springboot + Vue + shiro 实现前后端分离、权限控制
- SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架
- 七、spring boot 1.5.4 集成shiro+cas,实现单点登录和权限控制
- Spring boot 入门(四):集成 Shiro 实现登陆认证和权限管理
- SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架
- springboot整合shiro-spring-boot-web-starter实现前后端分离的跨域问题
- Spring Cloud之路:(七)SpringBoot+Shiro实现登录认证和权限管理
- SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架
- spring集成shiro实现登录认证自定义验证功能(认证采用国密SM4算法)
- Spring boot 集成 Kaptcha 实现前后端分离验证码功能
- 搭建spring-boot+vue前后端分离框架并实现登录功能
- 基于CAS的单点登录SSO[5]: 基于Springboot实现CAS客户端的前后端分离