前后端分离之接口登陆权限token

  随着业务的需求普通的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

在点查询 就可以获取到值了。

收工。

原文地址:https://www.cnblogs.com/iathanasy/p/8350156.html