随着业务的需求普通的springmvc+jsp已经不能满足我们的系统了,会逐渐把后台和前端展示分离开来,下面我们就来把普通的springmvc+jsp分为 springmvc只提供rest接口,前端用ajax请求接口渲染到html中。
后台提供接口是一个tomcat服务器
前台访问数据是nginx访问rest接口
但是有一个问题 ,发现没有。就是两个是不同的域名,所以存在跨域,下面我会把一些关键的代码贴出来。
首先解决接口访问跨域的问题。
自定义一个拦截请求的Filter
/** * post 跨域拦截 * @Project: children-watch-web-api * @Class JsonpPostFilter * @Description: TODO * @author cd 14163548@qq.com * @date 2018年1月10日 下午4:12:11 * @version V1.0 */ @Component public class JsonpPostFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; //String origin = (String) servletRequest.getRemoteHost() + ":"+ servletRequest.getRemotePort(); //构造头部信息 response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods","POST, GET, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers","x-requested-with,Authorization,X-Token"); response.setHeader("Access-Control-Allow-Credentials", "true"); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
然后再配置web.xml
<!-- 跨域配置--> <filter> <filter-name>cors</filter-name> <filter-class>com.axq.watch.web.api.config.JsonpPostFilter</filter-class> </filter> <filter-mapping> <filter-name>cors</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
这样就可以实现跨域访问了。
接下来就是登陆的问题,
思路:
1.用户输入账号密码,到后台查询,正确返回服务器生成的token,错误返回相应的错误信息。
2.用户拿到token保存到本地cookie.
3.用户要调用相应的接口需要把token传入头部。
4.后台获取访问的接口,看头部是否有token,在比对是否过期。
实现代码
token接口
/** * REST 鉴权 * @Project: children-watch-api * @Class TokenService * @Description: 登录用户的身份鉴权 * @author cd 14163548@qq.com * @date 2018年1月24日 上午11:43:28 * @version V1.0 */ public interface TokenLoginService { String createToken(String openid); boolean checkToken(String token); String getOpenId(String token); void deleteToken(String token); }
token接口登陆实现
/** * * @Project: children-watch-service * @Class TokenServiceImpl * @Description: 登录用户的身份鉴权 的实现 这里存入redis * @author cd 14163548@qq.com * @date 2018年1月24日 上午11:47:23 * @version V1.0 */ @Service("tokenLoginService") public class TokenLoginServiceImpl implements TokenLoginService { @Autowired private RedisCache redisCache; /** * 利用UUID创建Token(用户登录时,创建Token) */ @Override public String createToken(String openid) { String token = RandomString.createUUID().toUpperCase(); redisCache.set(token, openid); redisCache.expire(token, TokenConstant.TOKEN_EXPIRES_HOUR); return token; } @Override public boolean checkToken(String token) { return StringUtils.isNotBlank(token) && redisCache.hasKey(token); } @Override public void deleteToken(String token) { redisCache.del(token); } @Override public String getOpenId(String token) { if(checkToken(token)){ return (String) redisCache.get(token); } return ""; }
这里我是存入redis中的,方便集群
自定义一个注解,标识是否忽略REST安全性检查
/** * @Project: children-watch-web-api * @Class IgnoreSecurity 自定义注解 * @Description: 标识是否忽略REST安全性检查 * @author cd 14163548@qq.com * @date 2018年1月24日 下午12:13:21 * @version V1.0 */ @Target(ElementType.METHOD) //指明该类型的注解可以注解的程序元素的范围 @Retention(RetentionPolicy.RUNTIME) //指明了该Annotation被保留的时间长短 @Documented //指明拥有这个注解的元素可以被javadoc此类的工具文档化 public @interface IgnoreSecurity { }
自定义异常
/** * * @Project: children-watch-web-api * @Class TokenLoginException 自定义的RuntimeException * @Description: tokenlogin过期时抛出 * @author cd 14163548@qq.com * @date 2018年1月24日 下午2:28:41 * @version V1.0 */ public class TokenLoginException extends RuntimeException { private static final long serialVersionUID = 1L; private String msg; public TokenLoginException(String msg) { super(); this.msg = msg; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }
异常统一处理
/** * * @Project: children-watch-web-api * @Class ExceptionHandler 统一异常返回处理 * @Description: 统一异常返回处理 * @author cd 14163548@qq.com * @date 2018年1月24日 下午2:37:07 * @version V1.0 */ @ControllerAdvice @ResponseBody public class ExceptionHandle { private final static Logger logger = LoggerFactory.getLogger(ExceptionHandle.class); /** * 500 - Token is invaild */ @ExceptionHandler(TokenLoginException.class) public R handleTokenException(Exception e) { logger.error("Token is invaild...", e); return R.error("Token is invaild"); } /** * 500 - Internal Server Error */ @ExceptionHandler(Exception.class) public R handleException(Exception e) { logger.error("Internal Server Error...", e); return R.error("Internal Server Error"); } /** * 404 - Internal Server Error */ @ExceptionHandler(NotFoundException.class) public R notHandleException(Exception e) { logger.error("Not Found Error...", e); return R.error("Not Found Error"); } }
aop拦截访问是否忽略登陆检查
/** * * @Project: children-watch-web-api * @Class SecurityAspect 安全检查切面(是否登录检查) * @Description: 通过验证Token维持登录状态 * @author cd 14163548@qq.com * @date 2018年1月24日 下午12:23:19 * @version V1.0 */ @Component @Aspect public class SecurityAspect { /** Log4j日志处理 */ private static final Logger log = Logger.getLogger(SecurityAspect.class); @Autowired private TokenLoginService tokenLoginService; @Autowired private RedisCache redisCache; /** * 環繞通知 前後都通知 * aop检测注解为 RequestMapping 就调用此方法 * @param pjp * @return * @throws Throwable */ @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public Object execute(ProceedingJoinPoint pjp) throws Throwable { // 从切点上获取目标方法 MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); log.info("methodSignature : " + methodSignature); Method method = methodSignature.getMethod(); log.info("Method : " + method.getName() + " : " + method.isAnnotationPresent(IgnoreSecurity.class)); // 若目标方法忽略了安全性检查,则直接调用目标方法 if(method.isAnnotationPresent(IgnoreSecurity.class)){ // 调用目标方法 return pjp.proceed(); } //忽略 api接口测试安全性检查 if("getDocumentation".equalsIgnoreCase(method.getName())){ // 调用目标方法 return pjp.proceed(); } // 从 request header 中获取当前 token String token = HttpContextUtils.getHttpServletRequest().getHeader(TokenConstant.LONGIN_TOKEN_NAME); // 检查 token 有效性 if(!tokenLoginService.checkToken(token)){ String message = String.format("token [%s] is invalid", token); log.info("message : " + message); throw new TokenLoginException(message); } //每次调用接口就刷新过期时间 redisCache.expire(token, TokenConstant.TOKEN_EXPIRES_HOUR); // 调用目标方法 return pjp.proceed(); } }
一些工具类
public class HttpContextUtils { public static HttpServletRequest getHttpServletRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } } /** * rest * @Project: children-watch-commons * @Class TokenConstant * @Description: 接口登陆 token有效期 * @author cd 14163548@qq.com * @date 2018年1月24日 上午11:55:41 * @version V1.0 */ public class TokenConstant { /** * token有效期(秒) * 1天 */ public static final long TOKEN_EXPIRES_HOUR = 86400; /** 存放Token的header字段 */ public static final String LONGIN_TOKEN_NAME = "X-Token"; }
接口调用
@Controller public class WeChatLoginController extends BaseController{ /** * 本地测试 * @param openid * @return */ @RequestMapping("/login") @ResponseBody @IgnoreSecurity //忽略安全性检查 public R login(String openid){ logger.info("**** openid **** : " + openid); if(StringUtils.isNotBlank(openid)){ //创建token String createToken = tokenLoginService.createToken(openid); logger.info("**** Generate Token **** : " + createToken); return R.ok(createToken); } return R.Empty(); } /** * 获取openID * @return */ @RequestMapping("/openid") @ResponseBody public R getValue(HttpServletRequest request) { String openid = super.getOpenId(request); return R.ok(openid); } }
spring-context.xml中应配置扫描全部。
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- spring扫描com.axq.watch.web.*.controller下面所有带注解的类 --> <context:component-scan base-package="com.axq.watch.web" /> <!-- 默认servlet --> <mvc:default-servlet-handler /> <!-- 这个标签表示使用注解来驱动 --> <mvc:annotation-driven/> <!-- 支持Controller的AOP代理 --> <aop:aspectj-autoproxy /> <bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" /> <!-- 上传 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="utf-8"></property> <property name="maxUploadSize" value="10485760000"></property> <property name="maxInMemorySize" value="40960"></property> </bean> </beans>
前端代码
<html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width"> <script type="text/javascript" src="http://www.w3school.com.cn/jquery/jquery.js"></script> <script type="text/javascript"> $(document).ready(function(){ //登陆获取到的token var token = ""; $("#b01").click(function(){ $.ajax({ // url:"http://localhost:8081/rest/itemcat/list?callback=getMessage", url:"http://localhost:8081/children-watch-web-api/login", type:"post", cache:false, data:{openid:"ocHCAwMdYLevTBbcYrKh07FJJ56E"}, dataType:"json", /**beforeSend: function(request) { request.setRequestHeader("X-Token", token); },*/ success:function(data){ var html = data.msg; token = html ; $("#myDiv").html("token:"+html); }, error:function(){ alert("发生异常"); } }); }); $("#b02").click(function(){ $.ajax({ url:"http://localhost:8081/children-watch-web-api/openid", //url:"http://localhost:8081/children-watch-web-api/config/list", //url:"http://localhost:8081/children-watch-web-api/student/list", type:"get", cache:false, dataType:"json", beforeSend: function(request) { request.setRequestHeader("X-Token", token); }, success:function(data){ var html = data.msg; $("#myDiv").html("openId:"+html); }, error:function(e){ alert("发生异常"+e); }, complete: function(XMLHttpRequest, status) { //请求完成后最终执行参数 alert(status); } }); }); }); </script> </head> <body> <div id="myDiv"><h2>通过 AJAX 改变文本</h2></div> <button id="b01" type="button">登陆</button> <button id="b02" type="button">查询</button> </body> </html>
效果演示。
直接点查询没有token
点登陆 获取了token
在点查询 就可以获取到值了。
收工。