zoukankan      html  css  js  c++  java
  • 63

    简介
    登录模块很简单,前端发送账号密码的表单,后端接收验证后即可~

    淦!可是我想多了,于是有了以下几个问题(里面还包含网络安全问题):

    1.登录时的验证码

    2.自动登录的实现

    3.怎么维护前后端登录状态

    在这和大家分享下我实现此功能的过程,包括一些技术和心得

    1.登录时的验证码
    为什么要验证码,原因很简单,防止脚本无限次重复登录,来暴力破解用户密码或者攻击服务器

    验证码的出现,使得每次登录都有个动态变量需要输入,无法用脚本写死代码

    具体可以参考:滑动验证码的设计和理解

    2.自动登录的实现
    所谓自动登录,指的是当用户登录网站时勾选了自动登录,那么下次再访问网站就不需要输入账号密码直接登录了

    这说明,账号密码信息是必须保存在用户这边的,因此自动登录都是不安全的!(方便的代价呀)

    尽管不安全,但是我们也必须要尽力让它安全一点,有以下常用方法:

    1.账号密码加密保存

    2.降低自动登录后用户的权限(如果用户自动登录想改密码,想给我转钱等操作的话,就必须输入账号密码再登录一次!)

    3.进行ip检测(之前登录的ip小本本记着),如果发现和上次不一致,则不允许自动登录

    数据存储在前端哪里呢
    浏览器有3个经常保存数据的地方

    1.Cookie (我用这个)

    2.LocalStorage

    3.SessionStorage

    各位可以按F12直接观看

    如果你在多个大型网站下都按按F12,会发现SessionStorage基本没数据

    为啥,因为真的不好用,它并不是后台的session那样,生命周期是一个会话,这个SessionStorage存储的数据只限于该标签的页面

    意思是标签1和标签2即使是同个URL的网址,里面的数据都是不互通的(这有个毛用)

    那么LocalStorage存储的数据如何呢,答案是无限期本地存储

    不过后台无法操作这里的数据,只能由js代码操作(至于操作结果,完全看js,后端无法感知,不太可靠),我认为这里不适合保存敏感点的信息,因为前端的功能是展示,状态性的数据应该由后端直接掌控(后端能直接操作Cookie,保证完成任务)

    你看英雄所见略同,CSDN网站的用户密码也是存在Cookie的

    Token就是登录后的令牌(下一点会讲)

    所以用Cookie就对啦,具体实现都很简单,前端多个自动登录的选择,选择后多个参数传给后端,后端根据参数往Cookie里设置加密后的账号密码

    等下次访问时,用拦截器Interceptor进行拦截,检测是否要自动登录即可~

    3.如何维护前后端登录状态
    大家最先想到是用Session来维护,登录后在Session中存放用户信息,不过对分布式很不友好(什么,你说你用不到分布式,我也没用到,可是梦想还是要有的嘛),需要维护个分布式数据库来进行数据同步才行

    于是我用Token实现的,Token就是一串字符串,最适合API鉴权(例如SSO单点登录这种),俗称令牌

    好处就是账号密码用户输入一次就够了,特别是多个系统之间(一张身份的凭证都通用)

    当用户登录后,服务器就会生成一个Token放在Cookie中,之后用户的所有操作都带这个Token访问(将Token放入http头部)

    为什么要将Token放入头部
    1.能抵挡下简单的CSRF攻击

    2.浏览器跨域问题

    什么是CSRF攻击
    举个例子:我登录了A网站,A网站给我返回了一些Cookie信息,然后我再同一浏览器的另外标签访问了B网站,谁知这个B网站返回了一些攻击代码(向A网站发起一些请求,比如转钱给你,这时候由于是访问A网站,会附带A网站的Cookie,让一切都好像是我在访问一样),这个就是CSRF攻击

    但B网站并不知道A网站这么鸡贼,会在头部放了Token,所以这次攻击请求是的头部是没Token的,因此检测后发现非法,所以没得逞

    当然,这并不可靠,哪天B网站知道你头部放了Token,它研究A网站的js代码,清楚逻辑之后也加上,那就防不住了(所以说前端的东西一切都不可靠)

    正确做法应该是后端检测头部的Referer字段,每个网页里发起请求,请求的头部都会带有此字段,如

    这说明这个请求是从 http://localhost:8099/swr 中发出的

    B网站如果返回攻击代码,这里显示的事B网站的网址,判断出不是自家网站发出,就可以禁止访问

    浏览器跨域访问会发生什么
    说到跨域(自家网站去请求别人家的网站),得先了解什么是同源策略:

    同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

    它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

    所谓同源是指:域名、协议、端口相同。

    下表是相对于 http://www.laixiangran.cn/home/index.html 的同源检测结果:

    另外,同源策略又分为以下两种:

    DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
    XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。(就是ajax)
    咳咳,这里要说下第二种,其实设置一些参数之后,ajax访问时允许跨域请求的,甚至允许跨域时带上自身cookie

    但是,带上自己的Cookie多不安全,明明里面只有1,2个信息要传给对方,现在被人全看见了(不好不好),所以要将Token放入头部

    你说为啥不放到参数里,因为这会跟业务用的参数混淆,造成逻辑混乱(就好像你上学时要扔家里的垃圾,你不会放到书包里吧,都是手里提着的)

    每个请求都放token,所以要封装起来,例如我是将ajax封装起一个新的对象,然后在这个对象使用时添加Token

    当然啦,封装了ajax后还有其他好处(例如统一的成功,失败回调函数,统一的数据解析,统一的等待框等等),有兴趣的同学可以看下

    View Code
    预防XSS攻击,Filter知识讲解
    网上有些文章说,后端设置HttpOnly,让Cookie无法让js读写,可以防止XSS攻击。

    (⊙o⊙)…简直就是乱写,首先要了解下什么是XSS攻击

    Xss攻击是什么
    举个简单的例子,假设你前端有个地方可以输入,然后保存的数据库的地方

    用户A输入了以下东西

    然后这东西就到了后台,当作一串字符串保存了起来

    刚好你网站的html代码里,有个地方是显示用户输入过的东西的(例如评论区),然后上面的东西就被加载到html里面,如

    接下来每个人打开你的网站,都会弹出123的对话框,这就是XSS攻击

    怎么预防呢,在后端设置过滤器,对输入进行过滤,先上代码

    1 /**
    2 * @auther: NiceBin
    3 * @description: 系统的拦截器,注册在FilterConfig类中进行
    4 * 不能使用@WebFilter,因为Filter要排序
    5 * 1.对ServletRequest进行封装
    6 * 2.防止CSRF,检查http头的Referer字段
    7 * @date: 2020/12/15 15:32
    8 */
    9 @Component
    10 public class SystemFilter implements Filter {
    11 private final Logger logger = LoggerFactory.getLogger(SystemFilter.class);
    12 @Autowired
    13 private Environment environment;
    14
    15 @Override
    16 public void init(FilterConfig filterConfig) throws ServletException {
    17 logger.info(“系统拦截器SystemFilter开始加载”);
    18 }
    19
    20 @Override
    21 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    22 SystemHttpServletRequestWrapper requestWrapper = new SystemHttpServletRequestWrapper((HttpServletRequest) request);
    23
    24 //检测http的Referer字段,不允许跨域访问
    25 String hostPath = environment.getProperty(“server.host-path”);
    26 String referer = requestWrapper.getHeader(“Referer”);
    27 if(!Tool.isNull(referer)){
    28 if(referer.lastIndexOf(hostPath)!=0){
    29 ((HttpServletResponse)response).setStatus(HttpStatus.FORBIDDEN.value()); //设置错误状态码
    30 return;
    31 }
    32 }
    33 chain.doFilter(requestWrapper,response);
    34 }
    35
    36 @Override
    37 public void destroy() {
    38
    39 }
    40 }
    乍一看,是不是没发现哪里预防了XSS,其实正在的关键点在22行和33行代码,里面的SystemHttpServletRequestWrapper类才是关键,这个类是包装类,是替换参数里的ServletRequest类的,为的就是重写里面的方法,来达到预防XSS的目的,因为Spring也是根据ServletRequest类来进行前端参数读取的,所以它就是后端获得数据的源头

    1 /**
    2 * @auther: NiceBin
    3 * @description: 包装的httpServlet,进行以下增强
    4 * 1.将流数据取出保存,方便多次读出
    5 * 2.防止XSS攻击,修改读取数据的方法,过滤敏感字符
    6 * @date: 2020/4/23 19:50
    /
    8 public class SystemHttpServletRequestWrapper extends HttpServletRequestWrapper {
    9 private final byte[] body;
    10 private HttpServletRequest request;
    11
    12 public SystemHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    13 super(request);
    14 //打印属性
    15 //printRequestAll(request);
    16 body = HttpHelper.getBodyString(request).getBytes(Charset.forName(“UTF-8”)); //HttpHelper是我自己写的工具类
    17 this.request = request;
    18 }
    19
    20 @Override
    21 public BufferedReader getReader() throws IOException {
    22 return new BufferedReader(new InputStreamReader(getInputStream()));
    23 }
    24
    25 @Override
    26 public ServletInputStream getInputStream() throws IOException {
    27 final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    28 return new ServletInputStream() {
    29 @Override
    30 public boolean isFinished() {
    31 return false;
    32 }
    33
    34 @Override
    35 public boolean isReady() {
    36 return false;
    37 }
    38
    39 @Override
    40 public void setReadListener(ReadListener readListener) {
    41
    42 }
    43
    44 @Override
    45 public int read() throws IOException {
    46 return bais.read();
    47 }
    48 };
    49 }
    50
    51 /
    *
    52 * 可以打印出HttpServletRequest里属性的值
    53 * @param request
    54 */
    55 public void printRequestAll(HttpServletRequest request){
    56 Enumeration e = request.getHeaderNames();
    57 while (e.hasMoreElements()) {
    58 String name = (String) e.nextElement();
    59 String value = request.getHeader(name);
    60 System.out.println(name + " = " + value);
    61 }
    62 }
    63
    64 //以下为XSS预防
    65 @Override
    66 public String getParameter(String name) {
    67 String value = request.getParameter(name);
    68 if (!StringUtils.isEmpty(value)) {
    69 value = StringEscapeUtils.escapeHtml4(value);
    70 }
    71 return value;
    72 }
    73
    74 @Override
    75 public String[] getParameterValues(String name) {
    76 String[] parameterValues = super.getParameterValues(name);
    77 if (parameterValues == null) {
    78 return null;
    79 }
    80 for (int i = 0; i < parameterValues.length; i++) {
    81 String value = parameterValues[i];
    82 parameterValues[i] = StringEscapeUtils.escapeHtml4(value);
    83 }
    84 return parameterValues;
    85 }
    86 }
    HttpHelper工具类:

    View Code
    可以看到SystemHttpServletRequestWrapper的64行开始,重写了两个获取参数的方法,在获取参数的时候进行过滤即可~

    那64行往上是干啥的咧,这个是将ServletRequest里的数据读出来保存一份,因为ServletRequest里的数据流只能读取一次,很不方便

    啥意思呢,就是你在这个Filter里

    inputStream = request.getInputStream();
    reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(“UTF-8”)));
    String line = “”;
    while ((line = reader.readLine()) != null) {
    sb.append(line);
    }
    把数据读完,下个Filter再执行这些代码,就没数据了(从而导致Spring也接收不到数据)

    所以要保存起来,让后面的过滤器Filter和拦截器Interceptor快乐的读数据,没有后顾之忧(例如上面提到的验证码设计,如果你想用拦截器拦截,然后进行验证,则势必会读数据),既然封装ServletRequest这么重要,那必须得保证这个Filter第一个加载啊

    在Springboot中,Filter的排序用@Order是没用的,必须要用FilterRegistrationBean进行注册才能排序,如:

    1 /**
    2 * @auther: NiceBin
    3 * @description: 为了排序Filter,如果Filter有顺序要求
    4 * 那么需要在此注册,设置order(值越低优先级越高)
    5 * 其他没顺序需要的,可以@WebFilter注册
    6 * 如@WebFilter(filterName = “SecurityFilter”, urlPatterns = “/*”, asyncSupported = true)
    7 * @date: 2020/12/15 15:48
    /
    9 @Configuration
    10 public class FilterConfig {
    11
    12 @Autowired
    13 SystemFilter systemFilter;
    14 /
    *
    15 * 注册SystemFilter,顺序为1,任何其他filter不能比他优先
    16 * @return
    17 /
    18 @Bean
    19 public FilterRegistrationBean filterRegist(){
    20 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    21 filterRegistrationBean.setFilter(systemFilter);
    22 filterRegistrationBean.setName(“SystemFilter”);
    23 filterRegistrationBean.addUrlPatterns("/
    ");
    24 filterRegistrationBean.setAsyncSupported(true);
    25 filterRegistrationBean.setOrder(1);
    26 return filterRegistrationBean;
    27 }
    28 }
    当然了,如果你没用Springboot,那web.xml中定义的顺序就是Filter加载的顺序

    知识点提问:在我们之后的Filter或者Interceptor中,需要
    1 SystemHttpServletRequestWrapper requestWrapper = (SystemHttpServletRequestWrapper) request
    这样强制转换才能用吗?

    答案是不用的,你可以想想Spring也用了这个东西的,它怎么知道你定义的类叫什么名字,怎么强制转换,那么这设计到Java什么知识呢

    没错,就是Java的多态性,我们看以下代码

    public class Father {
    public void sayName(){
    System.out.println(“我是爸爸”);
    }
    }

    public class Son extends Father{
    public void sayName(){
    System.out.println(“我是儿子”);
    }
    }

    public class Test {

    @org.junit.Test
    public void test() throws Exception {
        Father father = new Son();
        otherMethod(father);
    }
    
    public void otherMethod(Father father){
        father.sayName();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    }
    输出:我是儿子

    答错了的留言,看看有多少小伙子~~ 接下来言归正传

    选择JWT生成Token
    JWT全称JSON Web Tokens 是一种规范化的 token(别人想的挺多挺全面的了,比你自己想的token要好一点)

    一个 JWT token 是一个字符串,它由三部分组成,头部、载荷与签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

    头部(header)
    头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA.)。

    例如:

    {
    “alg”: “HS256”,
    “typ”: “JWT”
    }
    然后用 Base64Url 编码得到头部,即 xxxxx。Base64Url编码后,才能在URL中正常传输(因为有人会把Token放在URL里…)

    载荷(Payload)
    载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换,如:

    {
    “sub”: “1”,

    “iss”: “http://localhost:8000/auth/login”,

    “iat”: 1451888119,

    “exp”: 1454516119,

    “nbf”: 1451888119,

    “jti”: “37c107e4609ddbcc9c096ea5ee76c667”,

    “aud”: “dev”

    }
    可以将载荷用别的方式加密一遍,这样别人得到了token也看不懂

    签名(Signature)
    签名时需要用到前面编码过的两个字符串,如果以 HMACSHA256 加密,就如下:

    HMACSHA256(

    base64UrlEncode(header) + "." +
    
    base64UrlEncode(payload),
    
    secret
    
    • 1
    • 2
    • 3
    • 4
    • 5

    )
    加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz。

    组合便可以得到 token:xxxxx.yyyyy.zzzzz。

    签名的作用:保证 JWT 没有被篡改过,原理如下:

    HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

    具体Java使用:

    com.auth0 java-jwt 3.10.2 io.jsonwebtoken jjwt 0.9.1 1 ** 2 * @auther: NiceBin 3 * @description: Jwt构造器,创建Token来进行身份记录 4 * jwt由3个部分构成:jwt头,有效载荷(主体,payLoad),签名 5 * @date: 2020/5/7 22:40 6 */ 7 public class JwtTool { 8 9 //以下为JwtTool生成时的主题 10 //登录是否还有效 11 public static final String SUBJECT_ONLINE_STATE = "online_state"; 12 13 //以下为载荷固定的Key值 14 //主题 15 public static final String SUBJECT = "subject"; 16 //发布时间 17 public static final String TIME_ISSUED = "timeIssued"; 18 //过期时间 19 public static final String EXPIRATION = "expiration"; 20 21 /** 22 * 生成token,参数都是载荷(自定义内容) 23 * 其中Map里为非必要数据,而其他参数为必要参数 24 * 25 * @param subject 主题,token生成干啥用的,用上面的常量作为参数 26 * @param liveTime 存活时间(秒单位),建议使用TimeUnit方便转换 27 * 如TimeUnit.HOURS.toSeconds(1);将1小时转为秒 = 3600 28 * @param claimMap 自定义荷载,可以为空 29 * @return 30 */ 31 public static String createToken(String subject, long liveTime, HashMap

    Https防止半路被截和重放攻击
    前面提到了Token就是身份令牌,可以相当于已登录一样进入系统,那么半路被人截了那就不好了

    所以要用Https协议,具体怎么设置大家自行百度吧(直接在tomcat操作的,不需要更改代码,证书也有免费的~)

    这里说下Https建立连接的过程,来看看为什么就不会被人截获了

    1.服务器先向CA(证书颁布机构)申请一个证书(证书里有自己的ip等等消息),然后在自己服务器设置好

    2.浏览器向服务器发送HTTPS请求,服务器将自己的证书发给浏览器

    3.浏览器拿到证书后,查看证书是否过期啊,ip是不是跟服务器的一样啊,跟检查身份证跟你长得像不像一样,检查没问题后,跟自己系统里的CA列表比对,看看是谁发的(找不到就报错,说证书不可信),比对成功后从列表里拿出对应的CA公钥解密证书(具体方法跟JWT的很像,浏览器用相同的算法和公钥对证书部分进行加密,看得到的值和证书的签名是否一致),得到服务器的公钥

    4.然后生成一个传输私钥,用服务器的公钥加密,发给服务器

    5.服务器用服务器的私钥解密,得到了传输秘钥,然后用传输秘钥进行加密要传送的信息发给浏览器

    6.浏览器用秘钥解密,然后用传输秘钥进行加密要传送的信息发给服务器(对称加密)

    7.重复5,6步骤直到结束

    以上哪个步骤黑客得到数据都看不懂

    至于为什么能防重放攻击,是因为Https通信自带序列号,如果黑客截取了浏览器的请求,重复发送一遍,那么序列号会一样,会被直接丢弃

  • 相关阅读:
    Intent
    What should we do next in general after collecting relevant data
    NOTE FOR Secure Friend Discovery in Mobile Social Networks
    missing pcap.h
    after building Android Source code
    plot point(one column)
    When talking to someone else, don't infer that is has been talked with others at first. It may bring repulsion to the person who is talking with you.
    进程基本知识
    Python input和raw_input的区别
    强制 code review:reviewboard+svn 的方案
  • 原文地址:https://www.cnblogs.com/gd11/p/14217484.html
Copyright © 2011-2022 走看看